@appkit/llamacpp-cli 1.5.0 → 1.7.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/CHANGELOG.md +20 -0
- package/MONITORING-ACCURACY-FIX.md +199 -0
- package/PER-PROCESS-METRICS.md +190 -0
- package/README.md +124 -9
- package/dist/cli.js +32 -7
- package/dist/cli.js.map +1 -1
- package/dist/commands/config.d.ts.map +1 -1
- package/dist/commands/config.js +15 -1
- package/dist/commands/config.js.map +1 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +12 -4
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/delete.js +12 -10
- package/dist/commands/delete.js.map +1 -1
- package/dist/commands/logs-all.d.ts +9 -0
- package/dist/commands/logs-all.d.ts.map +1 -0
- package/dist/commands/logs-all.js +209 -0
- package/dist/commands/logs-all.js.map +1 -0
- package/dist/commands/logs.d.ts +4 -0
- package/dist/commands/logs.d.ts.map +1 -1
- package/dist/commands/logs.js +108 -2
- package/dist/commands/logs.js.map +1 -1
- package/dist/commands/monitor.d.ts.map +1 -1
- package/dist/commands/monitor.js +51 -1
- package/dist/commands/monitor.js.map +1 -1
- package/dist/commands/ps.d.ts +3 -1
- package/dist/commands/ps.d.ts.map +1 -1
- package/dist/commands/ps.js +75 -5
- package/dist/commands/ps.js.map +1 -1
- package/dist/commands/rm.d.ts.map +1 -1
- package/dist/commands/rm.js +5 -12
- package/dist/commands/rm.js.map +1 -1
- package/dist/commands/server-show.d.ts.map +1 -1
- package/dist/commands/server-show.js +30 -3
- package/dist/commands/server-show.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +34 -7
- package/dist/commands/start.js.map +1 -1
- package/dist/commands/stop.js +3 -3
- package/dist/commands/stop.js.map +1 -1
- package/dist/lib/history-manager.d.ts +46 -0
- package/dist/lib/history-manager.d.ts.map +1 -0
- package/dist/lib/history-manager.js +157 -0
- package/dist/lib/history-manager.js.map +1 -0
- package/dist/lib/metrics-aggregator.d.ts +2 -1
- package/dist/lib/metrics-aggregator.d.ts.map +1 -1
- package/dist/lib/metrics-aggregator.js +15 -4
- package/dist/lib/metrics-aggregator.js.map +1 -1
- package/dist/lib/system-collector.d.ts +9 -4
- package/dist/lib/system-collector.d.ts.map +1 -1
- package/dist/lib/system-collector.js +29 -28
- package/dist/lib/system-collector.js.map +1 -1
- package/dist/tui/HistoricalMonitorApp.d.ts +5 -0
- package/dist/tui/HistoricalMonitorApp.d.ts.map +1 -0
- package/dist/tui/HistoricalMonitorApp.js +490 -0
- package/dist/tui/HistoricalMonitorApp.js.map +1 -0
- package/dist/tui/MonitorApp.d.ts.map +1 -1
- package/dist/tui/MonitorApp.js +84 -62
- package/dist/tui/MonitorApp.js.map +1 -1
- package/dist/tui/MultiServerMonitorApp.d.ts +1 -1
- package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -1
- package/dist/tui/MultiServerMonitorApp.js +293 -77
- package/dist/tui/MultiServerMonitorApp.js.map +1 -1
- package/dist/types/history-types.d.ts +30 -0
- package/dist/types/history-types.d.ts.map +1 -0
- package/dist/types/history-types.js +11 -0
- package/dist/types/history-types.js.map +1 -0
- package/dist/types/monitor-types.d.ts +1 -0
- package/dist/types/monitor-types.d.ts.map +1 -1
- package/dist/types/server-config.d.ts +1 -0
- package/dist/types/server-config.d.ts.map +1 -1
- package/dist/types/server-config.js.map +1 -1
- package/dist/utils/downsample-utils.d.ts +35 -0
- package/dist/utils/downsample-utils.d.ts.map +1 -0
- package/dist/utils/downsample-utils.js +107 -0
- package/dist/utils/downsample-utils.js.map +1 -0
- package/dist/utils/file-utils.d.ts +6 -0
- package/dist/utils/file-utils.d.ts.map +1 -1
- package/dist/utils/file-utils.js +38 -0
- package/dist/utils/file-utils.js.map +1 -1
- package/dist/utils/log-utils.d.ts +43 -0
- package/dist/utils/log-utils.d.ts.map +1 -0
- package/dist/utils/log-utils.js +190 -0
- package/dist/utils/log-utils.js.map +1 -0
- package/dist/utils/process-utils.d.ts +19 -1
- package/dist/utils/process-utils.d.ts.map +1 -1
- package/dist/utils/process-utils.js +79 -1
- package/dist/utils/process-utils.js.map +1 -1
- package/docs/images/.gitkeep +1 -0
- package/package.json +3 -1
- package/src/cli.ts +32 -7
- package/src/commands/config.ts +15 -1
- package/src/commands/create.ts +14 -5
- package/src/commands/delete.ts +10 -10
- package/src/commands/logs-all.ts +251 -0
- package/src/commands/logs.ts +138 -2
- package/src/commands/monitor.ts +21 -1
- package/src/commands/ps.ts +88 -5
- package/src/commands/rm.ts +5 -12
- package/src/commands/server-show.ts +35 -3
- package/src/commands/start.ts +35 -7
- package/src/commands/stop.ts +3 -3
- package/src/lib/history-manager.ts +172 -0
- package/src/lib/metrics-aggregator.ts +18 -5
- package/src/lib/system-collector.ts +31 -28
- package/src/tui/HistoricalMonitorApp.ts +548 -0
- package/src/tui/MonitorApp.ts +89 -64
- package/src/tui/MultiServerMonitorApp.ts +348 -103
- package/src/types/history-types.ts +39 -0
- package/src/types/monitor-types.ts +1 -0
- package/src/types/server-config.ts +1 -0
- package/src/utils/downsample-utils.ts +128 -0
- package/src/utils/file-utils.ts +40 -0
- package/src/utils/log-utils.ts +178 -0
- package/src/utils/process-utils.ts +85 -1
- package/test-load.sh +100 -0
- package/dist/tui/components/ErrorState.d.ts +0 -8
- package/dist/tui/components/ErrorState.d.ts.map +0 -1
- package/dist/tui/components/ErrorState.js +0 -22
- package/dist/tui/components/ErrorState.js.map +0 -1
- package/dist/tui/components/LoadingState.d.ts +0 -8
- package/dist/tui/components/LoadingState.d.ts.map +0 -1
- package/dist/tui/components/LoadingState.js +0 -21
- package/dist/tui/components/LoadingState.js.map +0 -1
|
@@ -3,6 +3,8 @@ import { ServerConfig } from '../types/server-config.js';
|
|
|
3
3
|
import { MetricsAggregator } from '../lib/metrics-aggregator.js';
|
|
4
4
|
import { SystemCollector } from '../lib/system-collector.js';
|
|
5
5
|
import { MonitorData, SystemMetrics } from '../types/monitor-types.js';
|
|
6
|
+
import { HistoryManager } from '../lib/history-manager.js';
|
|
7
|
+
import { createHistoricalUI, createMultiServerHistoricalUI } from './HistoricalMonitorApp.js';
|
|
6
8
|
|
|
7
9
|
type ViewMode = 'list' | 'detail';
|
|
8
10
|
|
|
@@ -14,14 +16,19 @@ interface ServerMonitorData {
|
|
|
14
16
|
|
|
15
17
|
export async function createMultiServerMonitorUI(
|
|
16
18
|
screen: blessed.Widgets.Screen,
|
|
17
|
-
servers: ServerConfig[]
|
|
19
|
+
servers: ServerConfig[],
|
|
20
|
+
_fromPs: boolean = false,
|
|
21
|
+
directJumpIndex?: number
|
|
18
22
|
): Promise<void> {
|
|
19
23
|
let updateInterval = 2000;
|
|
20
24
|
let intervalId: NodeJS.Timeout | null = null;
|
|
21
|
-
let viewMode: ViewMode = 'list';
|
|
22
|
-
let selectedServerIndex = 0;
|
|
25
|
+
let viewMode: ViewMode = directJumpIndex !== undefined ? 'detail' : 'list';
|
|
26
|
+
let selectedServerIndex = directJumpIndex ?? 0;
|
|
27
|
+
let selectedRowIndex = directJumpIndex ?? 0; // Track which row is highlighted in list view
|
|
23
28
|
let isLoading = false;
|
|
24
29
|
let lastSystemMetrics: SystemMetrics | null = null;
|
|
30
|
+
let cameFromDirectJump = directJumpIndex !== undefined; // Track if we entered via ps <id>
|
|
31
|
+
let inHistoricalView = false; // Track whether we're in historical view to prevent key conflicts
|
|
25
32
|
|
|
26
33
|
// Spinner animation
|
|
27
34
|
const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
@@ -30,11 +37,13 @@ export async function createMultiServerMonitorUI(
|
|
|
30
37
|
|
|
31
38
|
const systemCollector = new SystemCollector();
|
|
32
39
|
const aggregators = new Map<string, MetricsAggregator>();
|
|
40
|
+
const historyManagers = new Map<string, HistoryManager>();
|
|
33
41
|
const serverDataMap = new Map<string, ServerMonitorData>();
|
|
34
42
|
|
|
35
|
-
// Initialize aggregators for each server
|
|
43
|
+
// Initialize aggregators and history managers for each server
|
|
36
44
|
for (const server of servers) {
|
|
37
45
|
aggregators.set(server.id, new MetricsAggregator(server));
|
|
46
|
+
historyManagers.set(server.id, new HistoryManager(server.id));
|
|
38
47
|
serverDataMap.set(server.id, {
|
|
39
48
|
server,
|
|
40
49
|
data: null,
|
|
@@ -70,7 +79,7 @@ export async function createMultiServerMonitorUI(
|
|
|
70
79
|
return '[' + '█'.repeat(Math.max(0, filled)) + '░'.repeat(Math.max(0, empty)) + ']';
|
|
71
80
|
}
|
|
72
81
|
|
|
73
|
-
// Render system resources section
|
|
82
|
+
// Render system resources section (system-wide for list view)
|
|
74
83
|
function renderSystemResources(systemMetrics: SystemMetrics | null): string {
|
|
75
84
|
let content = '';
|
|
76
85
|
|
|
@@ -120,6 +129,97 @@ export async function createMultiServerMonitorUI(
|
|
|
120
129
|
return content;
|
|
121
130
|
}
|
|
122
131
|
|
|
132
|
+
// Render aggregate model resources (all running servers in list view)
|
|
133
|
+
function renderAggregateModelResources(): string {
|
|
134
|
+
let content = '';
|
|
135
|
+
|
|
136
|
+
content += '{bold}Model Resources{/bold}\n';
|
|
137
|
+
const termWidth = (screen.width as number) || 80;
|
|
138
|
+
const divider = '─'.repeat(termWidth - 2);
|
|
139
|
+
content += divider + '\n';
|
|
140
|
+
|
|
141
|
+
// Aggregate CPU and memory across all running servers (skip stopped servers)
|
|
142
|
+
let totalCpu = 0;
|
|
143
|
+
let totalMemoryBytes = 0;
|
|
144
|
+
let serverCount = 0;
|
|
145
|
+
|
|
146
|
+
for (const serverData of serverDataMap.values()) {
|
|
147
|
+
// Only count running servers with valid data
|
|
148
|
+
if (serverData.server.status === 'running' && serverData.data?.server && !serverData.data.server.stale) {
|
|
149
|
+
if (serverData.data.server.processCpuUsage !== undefined) {
|
|
150
|
+
totalCpu += serverData.data.server.processCpuUsage;
|
|
151
|
+
serverCount++;
|
|
152
|
+
}
|
|
153
|
+
if (serverData.data.server.processMemory !== undefined) {
|
|
154
|
+
totalMemoryBytes += serverData.data.server.processMemory;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (serverCount === 0) {
|
|
160
|
+
content += '{gray-fg}No running servers{/gray-fg}\n';
|
|
161
|
+
return content;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// CPU: Sum of all process CPU percentages
|
|
165
|
+
const cpuBar = createProgressBar(Math.min(totalCpu, 100));
|
|
166
|
+
content += `CPU: {cyan-fg}${cpuBar}{/cyan-fg} ${Math.round(totalCpu)}%`;
|
|
167
|
+
content += ` {gray-fg}(${serverCount} ${serverCount === 1 ? 'server' : 'servers'}){/gray-fg}\n`;
|
|
168
|
+
|
|
169
|
+
// Memory: Sum of all process memory
|
|
170
|
+
const totalMemoryGB = totalMemoryBytes / (1024 ** 3);
|
|
171
|
+
const estimatedMaxGB = serverCount * 8; // Assume ~8GB per server max
|
|
172
|
+
const memoryPercentage = Math.min((totalMemoryGB / estimatedMaxGB) * 100, 100);
|
|
173
|
+
const memoryBar = createProgressBar(memoryPercentage);
|
|
174
|
+
content += `Memory: {cyan-fg}${memoryBar}{/cyan-fg} ${totalMemoryGB.toFixed(2)} GB`;
|
|
175
|
+
content += ` {gray-fg}(${serverCount} ${serverCount === 1 ? 'server' : 'servers'}){/gray-fg}\n`;
|
|
176
|
+
|
|
177
|
+
return content;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Render model resources section (per-process for detail view)
|
|
181
|
+
function renderModelResources(data: MonitorData): string {
|
|
182
|
+
let content = '';
|
|
183
|
+
|
|
184
|
+
content += '{bold}Model Resources{/bold}\n';
|
|
185
|
+
const termWidth = (screen.width as number) || 80;
|
|
186
|
+
const divider = '─'.repeat(termWidth - 2);
|
|
187
|
+
content += divider + '\n';
|
|
188
|
+
|
|
189
|
+
// GPU: System-wide (can't get per-process on macOS)
|
|
190
|
+
if (data.system && data.system.gpuUsage !== undefined) {
|
|
191
|
+
const bar = createProgressBar(data.system.gpuUsage);
|
|
192
|
+
content += `GPU: {cyan-fg}${bar}{/cyan-fg} ${Math.round(data.system.gpuUsage)}% {gray-fg}(system){/gray-fg}`;
|
|
193
|
+
|
|
194
|
+
if (data.system.temperature !== undefined) {
|
|
195
|
+
content += ` - ${Math.round(data.system.temperature)}°C`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
content += '\n';
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// CPU: Per-process
|
|
202
|
+
if (data.server.processCpuUsage !== undefined) {
|
|
203
|
+
const bar = createProgressBar(data.server.processCpuUsage);
|
|
204
|
+
content += `CPU: {cyan-fg}${bar}{/cyan-fg} ${Math.round(data.server.processCpuUsage)}%\n`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Memory: Per-process
|
|
208
|
+
if (data.server.processMemory !== undefined) {
|
|
209
|
+
const memoryGB = data.server.processMemory / (1024 ** 3);
|
|
210
|
+
const estimatedMax = 8;
|
|
211
|
+
const memoryPercentage = Math.min((memoryGB / estimatedMax) * 100, 100);
|
|
212
|
+
const bar = createProgressBar(memoryPercentage);
|
|
213
|
+
content += `Memory: {cyan-fg}${bar}{/cyan-fg} ${memoryGB.toFixed(2)} GB\n`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (data.system && data.system.warnings && data.system.warnings.length > 0) {
|
|
217
|
+
content += `\n{yellow-fg}⚠ ${data.system.warnings.join(', ')}{/yellow-fg}\n`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return content;
|
|
221
|
+
}
|
|
222
|
+
|
|
123
223
|
// Show loading spinner
|
|
124
224
|
function showLoading(): void {
|
|
125
225
|
if (isLoading) return; // Already loading
|
|
@@ -169,53 +269,76 @@ export async function createMultiServerMonitorUI(
|
|
|
169
269
|
let content = '';
|
|
170
270
|
|
|
171
271
|
// Header
|
|
172
|
-
content += '{bold}{blue-fg}═══ llama.cpp
|
|
173
|
-
|
|
174
|
-
// Status line with optional spinner
|
|
175
|
-
const statusPlainText = 'Press 1-9 for details | [F] Filter | [Q] Quit';
|
|
176
|
-
const spinnerChar = isLoading ? spinnerFrames[spinnerFrameIndex] : '';
|
|
177
|
-
const spinnerText = spinnerChar ? ` {cyan-fg}${spinnerChar}{/cyan-fg}` : '';
|
|
178
|
-
|
|
179
|
-
content += `{gray-fg}${statusPlainText}${spinnerText}{/gray-fg}\n\n`;
|
|
272
|
+
content += '{bold}{blue-fg}═══ llama.cpp{/blue-fg}{/bold}\n\n';
|
|
180
273
|
|
|
181
274
|
// System resources
|
|
182
275
|
content += renderSystemResources(systemMetrics);
|
|
183
276
|
content += '\n';
|
|
184
277
|
|
|
278
|
+
// Aggregate model resources (CPU + memory for all running servers)
|
|
279
|
+
content += renderAggregateModelResources();
|
|
280
|
+
content += '\n';
|
|
281
|
+
|
|
185
282
|
// Server list header
|
|
186
283
|
const runningCount = servers.filter(s => s.status === 'running').length;
|
|
187
284
|
const stoppedCount = servers.filter(s => s.status !== 'running').length;
|
|
188
285
|
content += `{bold}Servers (${runningCount} running, ${stoppedCount} stopped){/bold}\n`;
|
|
189
|
-
content += '{gray-fg}
|
|
286
|
+
content += '{gray-fg}Use arrow keys to navigate, Enter to view details{/gray-fg}\n';
|
|
190
287
|
content += divider + '\n';
|
|
191
288
|
|
|
192
|
-
//
|
|
193
|
-
|
|
289
|
+
// Calculate Server ID column width (variable based on screen width)
|
|
290
|
+
// Fixed columns breakdown:
|
|
291
|
+
// indicator(1) + " │ "(3) + " │ "(3) + port(4) + " │ "(3) + status(6) + "│ "(2) +
|
|
292
|
+
// slots(5) + " │ "(3) + tok/s(6) + " │ "(3) + memory(7) = 46
|
|
293
|
+
const fixedColumnsWidth = 48; // Add 2 extra for safety margin
|
|
294
|
+
const minServerIdWidth = 20;
|
|
295
|
+
const maxServerIdWidth = 60;
|
|
296
|
+
const serverIdWidth = Math.max(
|
|
297
|
+
minServerIdWidth,
|
|
298
|
+
Math.min(maxServerIdWidth, termWidth - fixedColumnsWidth)
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
// Table header with variable Server ID width
|
|
302
|
+
const serverIdHeader = 'Server ID'.padEnd(serverIdWidth);
|
|
303
|
+
content += `{bold} │ ${serverIdHeader}│ Port │ Status │ Slots │ tok/s │ Memory{/bold}\n`;
|
|
194
304
|
content += divider + '\n';
|
|
195
305
|
|
|
196
306
|
// Server rows
|
|
197
307
|
servers.forEach((server, index) => {
|
|
198
308
|
const serverData = serverDataMap.get(server.id);
|
|
199
|
-
const
|
|
309
|
+
const isSelected = index === selectedRowIndex;
|
|
310
|
+
|
|
311
|
+
// Selection indicator (arrow for selected row)
|
|
312
|
+
// Use plain arrow for selected (will be white), colored for unselected indicator
|
|
313
|
+
const indicator = isSelected ? '►' : ' ';
|
|
200
314
|
|
|
201
|
-
// Server ID (truncate if
|
|
202
|
-
const serverId = server.id.padEnd(
|
|
315
|
+
// Server ID (variable width, truncate if longer than available space)
|
|
316
|
+
const serverId = server.id.padEnd(serverIdWidth).substring(0, serverIdWidth);
|
|
203
317
|
|
|
204
318
|
// Port
|
|
205
319
|
const port = server.port.toString().padStart(4);
|
|
206
320
|
|
|
207
|
-
// Status
|
|
321
|
+
// Status - Check actual server status first, then health
|
|
322
|
+
// Build two versions: colored for normal, plain for selected
|
|
208
323
|
let status = '';
|
|
209
|
-
|
|
324
|
+
let statusPlain = '';
|
|
325
|
+
if (server.status !== 'running') {
|
|
326
|
+
// Server is stopped according to config
|
|
327
|
+
status = '{gray-fg}○ OFF{/gray-fg} ';
|
|
328
|
+
statusPlain = '○ OFF ';
|
|
329
|
+
} else if (serverData?.data) {
|
|
330
|
+
// Server is running and we have data
|
|
210
331
|
if (serverData.data.server.healthy) {
|
|
211
332
|
status = '{green-fg}● RUN{/green-fg} ';
|
|
333
|
+
statusPlain = '● RUN ';
|
|
212
334
|
} else {
|
|
213
335
|
status = '{red-fg}● ERR{/red-fg} ';
|
|
336
|
+
statusPlain = '● ERR ';
|
|
214
337
|
}
|
|
215
|
-
} else if (server.status === 'running') {
|
|
216
|
-
status = '{yellow-fg}● ...{/yellow-fg} ';
|
|
217
338
|
} else {
|
|
218
|
-
|
|
339
|
+
// Server is running but no data yet (still loading)
|
|
340
|
+
status = '{yellow-fg}● ...{/yellow-fg} ';
|
|
341
|
+
statusPlain = '● ... ';
|
|
219
342
|
}
|
|
220
343
|
|
|
221
344
|
// Slots
|
|
@@ -247,13 +370,23 @@ export async function createMultiServerMonitorUI(
|
|
|
247
370
|
}
|
|
248
371
|
}
|
|
249
372
|
|
|
250
|
-
|
|
373
|
+
// Build row content - use plain status for selected rows
|
|
374
|
+
let rowContent = '';
|
|
375
|
+
if (isSelected) {
|
|
376
|
+
// Use color code 15 (bright white) with cyan background
|
|
377
|
+
// When white-bg worked, it was probably auto-selecting bright white fg
|
|
378
|
+
rowContent = `{cyan-bg}{15-fg}${indicator} │ ${serverId} │ ${port} │ ${statusPlain}│ ${slots} │ ${tokensPerSec} │ ${memory}{/15-fg}{/cyan-bg}`;
|
|
379
|
+
} else {
|
|
380
|
+
// Use colored status for normal rows
|
|
381
|
+
rowContent = `${indicator} │ ${serverId} │ ${port} │ ${status}│ ${slots} │ ${tokensPerSec} │ ${memory}`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
content += rowContent + '\n';
|
|
251
385
|
});
|
|
252
386
|
|
|
253
387
|
// Footer
|
|
254
388
|
content += '\n' + divider + '\n';
|
|
255
|
-
content += `{gray-fg}Updated: ${new Date().toLocaleTimeString()} | `;
|
|
256
|
-
content += `Interval: ${updateInterval}ms | [R]efresh [+/-]Speed{/gray-fg}`;
|
|
389
|
+
content += `{gray-fg}Updated: ${new Date().toLocaleTimeString()} | [H]istory [Q]uit{/gray-fg}`;
|
|
257
390
|
|
|
258
391
|
return content;
|
|
259
392
|
}
|
|
@@ -267,18 +400,50 @@ export async function createMultiServerMonitorUI(
|
|
|
267
400
|
let content = '';
|
|
268
401
|
|
|
269
402
|
// Header
|
|
270
|
-
content += `{bold}{blue-fg}═══
|
|
403
|
+
content += `{bold}{blue-fg}═══ ${server.id} (${server.port}){/blue-fg}{/bold}\n\n`;
|
|
271
404
|
|
|
272
|
-
//
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
405
|
+
// Check if server is stopped
|
|
406
|
+
if (server.status !== 'running') {
|
|
407
|
+
// Show stopped server configuration (no metrics)
|
|
408
|
+
content += '{bold}Server Information{/bold}\n';
|
|
409
|
+
content += divider + '\n';
|
|
410
|
+
content += `Status: {gray-fg}○ STOPPED{/gray-fg}\n`;
|
|
411
|
+
content += `Model: ${server.modelName}\n`;
|
|
412
|
+
const displayHost = server.host || '127.0.0.1';
|
|
413
|
+
content += `Endpoint: http://${displayHost}:${server.port}\n`;
|
|
414
|
+
content += '\n';
|
|
276
415
|
|
|
277
|
-
|
|
416
|
+
content += '{bold}Configuration{/bold}\n';
|
|
417
|
+
content += divider + '\n';
|
|
418
|
+
content += `Threads: ${server.threads}\n`;
|
|
419
|
+
content += `Context: ${server.ctxSize} tokens\n`;
|
|
420
|
+
content += `GPU Layers: ${server.gpuLayers}\n`;
|
|
421
|
+
if (server.verbose) {
|
|
422
|
+
content += `Verbose: Enabled\n`;
|
|
423
|
+
}
|
|
424
|
+
if (server.customFlags && server.customFlags.length > 0) {
|
|
425
|
+
content += `Flags: ${server.customFlags.join(', ')}\n`;
|
|
426
|
+
}
|
|
427
|
+
content += '\n';
|
|
278
428
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
429
|
+
if (server.lastStarted) {
|
|
430
|
+
content += '{bold}Last Activity{/bold}\n';
|
|
431
|
+
content += divider + '\n';
|
|
432
|
+
content += `Started: ${new Date(server.lastStarted).toLocaleString()}\n`;
|
|
433
|
+
if (server.lastStopped) {
|
|
434
|
+
content += `Stopped: ${new Date(server.lastStopped).toLocaleString()}\n`;
|
|
435
|
+
}
|
|
436
|
+
content += '\n';
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
content += '{bold}Quick Actions{/bold}\n';
|
|
440
|
+
content += divider + '\n';
|
|
441
|
+
content += `{dim}Start server: llamacpp server start ${server.port}{/dim}\n`;
|
|
442
|
+
content += `{dim}Update config: llamacpp server config ${server.port} [options]{/dim}\n`;
|
|
443
|
+
content += `{dim}View logs: llamacpp server logs ${server.port}{/dim}\n`;
|
|
444
|
+
|
|
445
|
+
return content;
|
|
446
|
+
}
|
|
282
447
|
|
|
283
448
|
if (!serverData?.data) {
|
|
284
449
|
content += '{yellow-fg}Loading server data...{/yellow-fg}\n';
|
|
@@ -287,6 +452,10 @@ export async function createMultiServerMonitorUI(
|
|
|
287
452
|
|
|
288
453
|
const data = serverData.data;
|
|
289
454
|
|
|
455
|
+
// Model resources (per-process)
|
|
456
|
+
content += renderModelResources(data);
|
|
457
|
+
content += '\n';
|
|
458
|
+
|
|
290
459
|
// Server Information
|
|
291
460
|
content += '{bold}Server Information{/bold}\n';
|
|
292
461
|
content += divider + '\n';
|
|
@@ -308,21 +477,7 @@ export async function createMultiServerMonitorUI(
|
|
|
308
477
|
|
|
309
478
|
// Handle null host (legacy configs) by defaulting to 127.0.0.1
|
|
310
479
|
const displayHost = server.host || '127.0.0.1';
|
|
311
|
-
content += `Endpoint: http://${displayHost}:${server.port}`;
|
|
312
|
-
|
|
313
|
-
// Add actual process memory (if available)
|
|
314
|
-
if (data.server.processMemory) {
|
|
315
|
-
const bytes = data.server.processMemory;
|
|
316
|
-
let memStr;
|
|
317
|
-
if (bytes >= 1024 * 1024 * 1024) {
|
|
318
|
-
memStr = `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
|
|
319
|
-
} else {
|
|
320
|
-
memStr = `${Math.round(bytes / (1024 * 1024))} MB`;
|
|
321
|
-
}
|
|
322
|
-
content += ` Memory: ${memStr}\n`;
|
|
323
|
-
} else {
|
|
324
|
-
content += '\n';
|
|
325
|
-
}
|
|
480
|
+
content += `Endpoint: http://${displayHost}:${server.port}\n`;
|
|
326
481
|
|
|
327
482
|
content += `Slots: ${data.server.activeSlots} active / ${data.server.totalSlots} total\n`;
|
|
328
483
|
content += '\n';
|
|
@@ -377,8 +532,7 @@ export async function createMultiServerMonitorUI(
|
|
|
377
532
|
|
|
378
533
|
// Footer
|
|
379
534
|
content += divider + '\n';
|
|
380
|
-
content += `{gray-fg}Updated: ${data.lastUpdated.toLocaleTimeString()} | `;
|
|
381
|
-
content += `Interval: ${updateInterval}ms | [R]efresh [+/-]Speed{/gray-fg}`;
|
|
535
|
+
content += `{gray-fg}Updated: ${data.lastUpdated.toLocaleTimeString()} | [H]istory [ESC] Back [Q]uit{/gray-fg}`;
|
|
382
536
|
|
|
383
537
|
return content;
|
|
384
538
|
}
|
|
@@ -390,51 +544,68 @@ export async function createMultiServerMonitorUI(
|
|
|
390
544
|
// This prevents spawning multiple macmon processes
|
|
391
545
|
const systemMetricsPromise = systemCollector.collectSystemMetrics();
|
|
392
546
|
|
|
393
|
-
// Batch collect process memory for ALL servers in
|
|
547
|
+
// Batch collect process memory and CPU for ALL servers in parallel
|
|
394
548
|
// This prevents spawning multiple top processes (5x speedup)
|
|
395
|
-
const { getBatchProcessMemory } = await import('../utils/process-utils.js');
|
|
549
|
+
const { getBatchProcessMemory, getBatchProcessCpu } = await import('../utils/process-utils.js');
|
|
396
550
|
const pids = servers.filter(s => s.pid).map(s => s.pid!);
|
|
397
551
|
const memoryMapPromise = pids.length > 0
|
|
398
552
|
? getBatchProcessMemory(pids)
|
|
399
553
|
: Promise.resolve(new Map<number, number | null>());
|
|
554
|
+
const cpuMapPromise = pids.length > 0
|
|
555
|
+
? getBatchProcessCpu(pids)
|
|
556
|
+
: Promise.resolve(new Map<number, number | null>());
|
|
400
557
|
|
|
401
|
-
// Wait for
|
|
402
|
-
const memoryMap = await memoryMapPromise;
|
|
403
|
-
|
|
404
|
-
// Collect server metrics only
|
|
405
|
-
const promises = servers
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
server
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
558
|
+
// Wait for both batches to complete
|
|
559
|
+
const [memoryMap, cpuMap] = await Promise.all([memoryMapPromise, cpuMapPromise]);
|
|
560
|
+
|
|
561
|
+
// Collect server metrics only for RUNNING servers (skip stopped servers)
|
|
562
|
+
const promises = servers
|
|
563
|
+
.filter(server => server.status === 'running')
|
|
564
|
+
.map(async (server) => {
|
|
565
|
+
const aggregator = aggregators.get(server.id)!;
|
|
566
|
+
try {
|
|
567
|
+
// Use collectServerMetrics instead of collectMonitorData
|
|
568
|
+
// to avoid spawning macmon per server
|
|
569
|
+
// Pass pre-fetched memory and CPU to avoid spawning top per server
|
|
570
|
+
const serverMetrics = await aggregator.collectServerMetrics(
|
|
571
|
+
server,
|
|
572
|
+
server.pid ? memoryMap.get(server.pid) ?? null : null,
|
|
573
|
+
server.pid ? cpuMap.get(server.pid) ?? null : null
|
|
574
|
+
);
|
|
575
|
+
|
|
576
|
+
// Build MonitorData manually with shared system metrics
|
|
577
|
+
const data: MonitorData = {
|
|
578
|
+
server: serverMetrics,
|
|
579
|
+
system: undefined, // Will be set after system metrics resolve
|
|
580
|
+
lastUpdated: new Date(),
|
|
581
|
+
updateInterval,
|
|
582
|
+
consecutiveFailures: 0,
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
serverDataMap.set(server.id, {
|
|
586
|
+
server,
|
|
587
|
+
data,
|
|
588
|
+
error: null,
|
|
589
|
+
});
|
|
590
|
+
} catch (err) {
|
|
591
|
+
serverDataMap.set(server.id, {
|
|
592
|
+
server,
|
|
593
|
+
data: null,
|
|
594
|
+
error: err instanceof Error ? err.message : 'Unknown error',
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
});
|
|
424
598
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
});
|
|
430
|
-
} catch (err) {
|
|
599
|
+
// Set null data for stopped servers (no metrics collection)
|
|
600
|
+
servers
|
|
601
|
+
.filter(server => server.status !== 'running')
|
|
602
|
+
.forEach(server => {
|
|
431
603
|
serverDataMap.set(server.id, {
|
|
432
604
|
server,
|
|
433
605
|
data: null,
|
|
434
|
-
error:
|
|
606
|
+
error: null,
|
|
435
607
|
});
|
|
436
|
-
}
|
|
437
|
-
});
|
|
608
|
+
});
|
|
438
609
|
|
|
439
610
|
// Wait for both system metrics and server metrics to complete
|
|
440
611
|
const systemMetrics = await systemMetricsPromise;
|
|
@@ -450,6 +621,19 @@ export async function createMultiServerMonitorUI(
|
|
|
450
621
|
}
|
|
451
622
|
}
|
|
452
623
|
|
|
624
|
+
// Append to history for each server (silent failure)
|
|
625
|
+
// Only save history for servers that are healthy and not stale
|
|
626
|
+
for (const [serverId, serverData] of serverDataMap) {
|
|
627
|
+
if (serverData.data && !serverData.data.server.stale && serverData.data.server.healthy) {
|
|
628
|
+
const manager = historyManagers.get(serverId);
|
|
629
|
+
manager?.appendSnapshot(serverData.data.server, serverData.data.system)
|
|
630
|
+
.catch(err => {
|
|
631
|
+
// Don't interrupt monitoring on history write failure
|
|
632
|
+
console.error(`Failed to save history for ${serverId}:`, err);
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
453
637
|
// Render once with complete data
|
|
454
638
|
let content = '';
|
|
455
639
|
if (viewMode === 'list') {
|
|
@@ -485,12 +669,32 @@ export async function createMultiServerMonitorUI(
|
|
|
485
669
|
intervalId = setInterval(fetchData, updateInterval);
|
|
486
670
|
}
|
|
487
671
|
|
|
488
|
-
// Keyboard shortcuts - List view
|
|
489
|
-
screen.key(['
|
|
490
|
-
|
|
491
|
-
|
|
672
|
+
// Keyboard shortcuts - List view navigation with arrow keys
|
|
673
|
+
screen.key(['up', 'k'], () => {
|
|
674
|
+
if (viewMode === 'list') {
|
|
675
|
+
selectedRowIndex = Math.max(0, selectedRowIndex - 1);
|
|
676
|
+
// Re-render immediately for responsive feel
|
|
677
|
+
const content = renderListView(lastSystemMetrics);
|
|
678
|
+
contentBox.setContent(content);
|
|
679
|
+
screen.render();
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
|
|
683
|
+
screen.key(['down', 'j'], () => {
|
|
684
|
+
if (viewMode === 'list') {
|
|
685
|
+
selectedRowIndex = Math.min(servers.length - 1, selectedRowIndex + 1);
|
|
686
|
+
// Re-render immediately for responsive feel
|
|
687
|
+
const content = renderListView(lastSystemMetrics);
|
|
688
|
+
contentBox.setContent(content);
|
|
689
|
+
screen.render();
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
|
|
693
|
+
// Enter key to view details for selected server
|
|
694
|
+
screen.key(['enter'], () => {
|
|
695
|
+
if (viewMode === 'list') {
|
|
492
696
|
showLoading();
|
|
493
|
-
selectedServerIndex =
|
|
697
|
+
selectedServerIndex = selectedRowIndex;
|
|
494
698
|
viewMode = 'detail';
|
|
495
699
|
fetchData();
|
|
496
700
|
}
|
|
@@ -498,27 +702,68 @@ export async function createMultiServerMonitorUI(
|
|
|
498
702
|
|
|
499
703
|
// Keyboard shortcuts - Detail view
|
|
500
704
|
screen.key(['escape'], () => {
|
|
705
|
+
// Don't handle ESC if we're in historical view - let historical view handle it
|
|
706
|
+
if (inHistoricalView) return;
|
|
707
|
+
|
|
501
708
|
if (viewMode === 'detail') {
|
|
502
709
|
showLoading();
|
|
503
710
|
viewMode = 'list';
|
|
711
|
+
cameFromDirectJump = false; // Clear direct jump flag when returning to list
|
|
504
712
|
fetchData();
|
|
713
|
+
} else if (viewMode === 'list') {
|
|
714
|
+
// ESC in list view - exit
|
|
715
|
+
showLoading();
|
|
716
|
+
if (intervalId) clearInterval(intervalId);
|
|
717
|
+
if (spinnerIntervalId) clearInterval(spinnerIntervalId);
|
|
718
|
+
setTimeout(() => {
|
|
719
|
+
screen.destroy();
|
|
720
|
+
process.exit(0);
|
|
721
|
+
}, 100);
|
|
505
722
|
}
|
|
506
723
|
});
|
|
507
724
|
|
|
508
725
|
// Keyboard shortcuts - Common
|
|
509
|
-
screen.key(['r', 'R'], () => {
|
|
510
|
-
showLoading();
|
|
511
|
-
fetchData();
|
|
512
|
-
});
|
|
513
726
|
|
|
514
|
-
screen.key(['
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
727
|
+
screen.key(['h', 'H'], async () => {
|
|
728
|
+
// Prevent entering historical view if already there
|
|
729
|
+
if (inHistoricalView) return;
|
|
730
|
+
|
|
731
|
+
// Keep polling in background for live historical updates
|
|
732
|
+
// Stop spinner if running
|
|
733
|
+
if (spinnerIntervalId) clearInterval(spinnerIntervalId);
|
|
518
734
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
735
|
+
// Remove current content box
|
|
736
|
+
screen.remove(contentBox);
|
|
737
|
+
|
|
738
|
+
// Mark that we're in historical view
|
|
739
|
+
inHistoricalView = true;
|
|
740
|
+
|
|
741
|
+
if (viewMode === 'list') {
|
|
742
|
+
// Show multi-server historical view
|
|
743
|
+
await createMultiServerHistoricalUI(screen, servers, selectedServerIndex, () => {
|
|
744
|
+
// Mark that we've left historical view
|
|
745
|
+
inHistoricalView = false;
|
|
746
|
+
// Re-attach content box when returning from history
|
|
747
|
+
screen.append(contentBox);
|
|
748
|
+
// Re-render the list view
|
|
749
|
+
const content = renderListView(lastSystemMetrics);
|
|
750
|
+
contentBox.setContent(content);
|
|
751
|
+
screen.render();
|
|
752
|
+
});
|
|
753
|
+
} else {
|
|
754
|
+
// Show single-server historical view for selected server
|
|
755
|
+
const selectedServer = servers[selectedServerIndex];
|
|
756
|
+
await createHistoricalUI(screen, selectedServer, () => {
|
|
757
|
+
// Mark that we've left historical view
|
|
758
|
+
inHistoricalView = false;
|
|
759
|
+
// Re-attach content box when returning from history
|
|
760
|
+
screen.append(contentBox);
|
|
761
|
+
// Re-render the detail view
|
|
762
|
+
const content = renderDetailView(lastSystemMetrics);
|
|
763
|
+
contentBox.setContent(content);
|
|
764
|
+
screen.render();
|
|
765
|
+
});
|
|
766
|
+
}
|
|
522
767
|
});
|
|
523
768
|
|
|
524
769
|
screen.key(['q', 'Q', 'C-c'], () => {
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
// Historical monitoring data types
|
|
2
|
+
|
|
3
|
+
export interface HistorySnapshot {
|
|
4
|
+
timestamp: number; // Unix timestamp in milliseconds
|
|
5
|
+
server: {
|
|
6
|
+
healthy: boolean;
|
|
7
|
+
uptime?: string;
|
|
8
|
+
activeSlots: number;
|
|
9
|
+
idleSlots: number;
|
|
10
|
+
totalSlots: number;
|
|
11
|
+
avgPromptSpeed?: number; // Tokens per second
|
|
12
|
+
avgGenerateSpeed?: number; // Tokens per second
|
|
13
|
+
processMemory?: number; // Bytes (RSS)
|
|
14
|
+
processCpuUsage?: number; // Percentage (0-100+) from ps
|
|
15
|
+
};
|
|
16
|
+
system?: {
|
|
17
|
+
gpuUsage?: number; // Percentage (0-100)
|
|
18
|
+
cpuUsage?: number; // Percentage (0-100)
|
|
19
|
+
aneUsage?: number; // Percentage (0-100)
|
|
20
|
+
temperature?: number; // Celsius
|
|
21
|
+
memoryUsed: number; // Bytes
|
|
22
|
+
memoryTotal: number; // Bytes
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface HistoryData {
|
|
27
|
+
serverId: string;
|
|
28
|
+
snapshots: HistorySnapshot[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type TimeWindow = '1h' | '6h' | '24h';
|
|
32
|
+
|
|
33
|
+
export const TIME_WINDOW_HOURS: Record<TimeWindow, number> = {
|
|
34
|
+
'1h': 1,
|
|
35
|
+
'6h': 6,
|
|
36
|
+
'24h': 24,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const TIME_WINDOWS: TimeWindow[] = ['1h', '6h', '24h'];
|
|
@@ -22,6 +22,7 @@ export interface ServerConfig {
|
|
|
22
22
|
createdAt: string; // ISO timestamp
|
|
23
23
|
lastStarted?: string; // ISO timestamp
|
|
24
24
|
lastStopped?: string; // ISO timestamp
|
|
25
|
+
metalMemoryMB?: number; // Metal (GPU) memory allocated in MB (parsed from logs)
|
|
25
26
|
|
|
26
27
|
// launchctl metadata
|
|
27
28
|
plistPath: string; // Full path to plist file
|