@appkit/llamacpp-cli 1.4.1 → 1.6.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 +21 -0
- package/MONITORING-ACCURACY-FIX.md +199 -0
- package/PER-PROCESS-METRICS.md +190 -0
- package/README.md +136 -1
- package/dist/cli.js +21 -4
- package/dist/cli.js.map +1 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +12 -3
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/monitor.d.ts +2 -0
- package/dist/commands/monitor.d.ts.map +1 -0
- package/dist/commands/monitor.js +126 -0
- package/dist/commands/monitor.js.map +1 -0
- 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/server-show.d.ts.map +1 -1
- package/dist/commands/server-show.js +10 -3
- package/dist/commands/server-show.js.map +1 -1
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +14 -2
- package/dist/commands/start.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 +40 -0
- package/dist/lib/metrics-aggregator.d.ts.map +1 -0
- package/dist/lib/metrics-aggregator.js +211 -0
- package/dist/lib/metrics-aggregator.js.map +1 -0
- package/dist/lib/system-collector.d.ts +80 -0
- package/dist/lib/system-collector.d.ts.map +1 -0
- package/dist/lib/system-collector.js +311 -0
- package/dist/lib/system-collector.js.map +1 -0
- 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 +4 -0
- package/dist/tui/MonitorApp.d.ts.map +1 -0
- package/dist/tui/MonitorApp.js +315 -0
- package/dist/tui/MonitorApp.js.map +1 -0
- package/dist/tui/MultiServerMonitorApp.d.ts +4 -0
- package/dist/tui/MultiServerMonitorApp.d.ts.map +1 -0
- package/dist/tui/MultiServerMonitorApp.js +712 -0
- package/dist/tui/MultiServerMonitorApp.js.map +1 -0
- 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 +123 -0
- package/dist/types/monitor-types.d.ts.map +1 -0
- package/dist/types/monitor-types.js +3 -0
- package/dist/types/monitor-types.js.map +1 -0
- 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/process-utils.d.ts +35 -2
- package/dist/utils/process-utils.d.ts.map +1 -1
- package/dist/utils/process-utils.js +220 -25
- package/dist/utils/process-utils.js.map +1 -1
- package/docs/images/.gitkeep +1 -0
- package/package.json +5 -1
- package/src/cli.ts +21 -4
- package/src/commands/create.ts +14 -4
- package/src/commands/monitor.ts +110 -0
- package/src/commands/ps.ts +88 -5
- package/src/commands/server-show.ts +10 -3
- package/src/commands/start.ts +15 -2
- package/src/lib/history-manager.ts +172 -0
- package/src/lib/metrics-aggregator.ts +257 -0
- package/src/lib/system-collector.ts +315 -0
- package/src/tui/HistoricalMonitorApp.ts +548 -0
- package/src/tui/MonitorApp.ts +386 -0
- package/src/tui/MultiServerMonitorApp.ts +792 -0
- package/src/types/history-types.ts +39 -0
- package/src/types/monitor-types.ts +162 -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/process-utils.ts +243 -25
- package/test-load.sh +100 -0
|
@@ -0,0 +1,712 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.createMultiServerMonitorUI = createMultiServerMonitorUI;
|
|
40
|
+
const blessed_1 = __importDefault(require("blessed"));
|
|
41
|
+
const metrics_aggregator_js_1 = require("../lib/metrics-aggregator.js");
|
|
42
|
+
const system_collector_js_1 = require("../lib/system-collector.js");
|
|
43
|
+
const history_manager_js_1 = require("../lib/history-manager.js");
|
|
44
|
+
const HistoricalMonitorApp_js_1 = require("./HistoricalMonitorApp.js");
|
|
45
|
+
async function createMultiServerMonitorUI(screen, servers, _fromPs = false, directJumpIndex) {
|
|
46
|
+
let updateInterval = 2000;
|
|
47
|
+
let intervalId = null;
|
|
48
|
+
let viewMode = directJumpIndex !== undefined ? 'detail' : 'list';
|
|
49
|
+
let selectedServerIndex = directJumpIndex ?? 0;
|
|
50
|
+
let selectedRowIndex = directJumpIndex ?? 0; // Track which row is highlighted in list view
|
|
51
|
+
let isLoading = false;
|
|
52
|
+
let lastSystemMetrics = null;
|
|
53
|
+
let cameFromDirectJump = directJumpIndex !== undefined; // Track if we entered via ps <id>
|
|
54
|
+
let inHistoricalView = false; // Track whether we're in historical view to prevent key conflicts
|
|
55
|
+
// Spinner animation
|
|
56
|
+
const spinnerFrames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
57
|
+
let spinnerFrameIndex = 0;
|
|
58
|
+
let spinnerIntervalId = null;
|
|
59
|
+
const systemCollector = new system_collector_js_1.SystemCollector();
|
|
60
|
+
const aggregators = new Map();
|
|
61
|
+
const historyManagers = new Map();
|
|
62
|
+
const serverDataMap = new Map();
|
|
63
|
+
// Initialize aggregators and history managers for each server
|
|
64
|
+
for (const server of servers) {
|
|
65
|
+
aggregators.set(server.id, new metrics_aggregator_js_1.MetricsAggregator(server));
|
|
66
|
+
historyManagers.set(server.id, new history_manager_js_1.HistoryManager(server.id));
|
|
67
|
+
serverDataMap.set(server.id, {
|
|
68
|
+
server,
|
|
69
|
+
data: null,
|
|
70
|
+
error: null,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
// Single scrollable content box
|
|
74
|
+
const contentBox = blessed_1.default.box({
|
|
75
|
+
top: 0,
|
|
76
|
+
left: 0,
|
|
77
|
+
width: '100%',
|
|
78
|
+
height: '100%',
|
|
79
|
+
tags: true,
|
|
80
|
+
scrollable: true,
|
|
81
|
+
alwaysScroll: true,
|
|
82
|
+
keys: true,
|
|
83
|
+
vi: true,
|
|
84
|
+
mouse: true,
|
|
85
|
+
scrollbar: {
|
|
86
|
+
ch: '█',
|
|
87
|
+
style: {
|
|
88
|
+
fg: 'blue',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
screen.append(contentBox);
|
|
93
|
+
// Helper to create progress bar
|
|
94
|
+
function createProgressBar(percentage, width = 30) {
|
|
95
|
+
const filled = Math.round((percentage / 100) * width);
|
|
96
|
+
const empty = width - filled;
|
|
97
|
+
return '[' + '█'.repeat(Math.max(0, filled)) + '░'.repeat(Math.max(0, empty)) + ']';
|
|
98
|
+
}
|
|
99
|
+
// Render system resources section (system-wide for list view)
|
|
100
|
+
function renderSystemResources(systemMetrics) {
|
|
101
|
+
let content = '';
|
|
102
|
+
content += '{bold}System Resources{/bold}\n';
|
|
103
|
+
const termWidth = screen.width || 80;
|
|
104
|
+
const divider = '─'.repeat(termWidth - 2);
|
|
105
|
+
content += divider + '\n';
|
|
106
|
+
if (systemMetrics) {
|
|
107
|
+
if (systemMetrics.gpuUsage !== undefined) {
|
|
108
|
+
const bar = createProgressBar(systemMetrics.gpuUsage);
|
|
109
|
+
content += `GPU: {cyan-fg}${bar}{/cyan-fg} ${Math.round(systemMetrics.gpuUsage)}%`;
|
|
110
|
+
if (systemMetrics.temperature !== undefined) {
|
|
111
|
+
content += ` - ${Math.round(systemMetrics.temperature)}°C`;
|
|
112
|
+
}
|
|
113
|
+
content += '\n';
|
|
114
|
+
}
|
|
115
|
+
if (systemMetrics.cpuUsage !== undefined) {
|
|
116
|
+
const bar = createProgressBar(systemMetrics.cpuUsage);
|
|
117
|
+
content += `CPU: {cyan-fg}${bar}{/cyan-fg} ${Math.round(systemMetrics.cpuUsage)}%\n`;
|
|
118
|
+
}
|
|
119
|
+
if (systemMetrics.aneUsage !== undefined && systemMetrics.aneUsage > 1) {
|
|
120
|
+
const bar = createProgressBar(systemMetrics.aneUsage);
|
|
121
|
+
content += `ANE: {cyan-fg}${bar}{/cyan-fg} ${Math.round(systemMetrics.aneUsage)}%\n`;
|
|
122
|
+
}
|
|
123
|
+
if (systemMetrics.memoryTotal > 0) {
|
|
124
|
+
const memoryUsedGB = systemMetrics.memoryUsed / (1024 ** 3);
|
|
125
|
+
const memoryTotalGB = systemMetrics.memoryTotal / (1024 ** 3);
|
|
126
|
+
const memoryPercentage = (systemMetrics.memoryUsed / systemMetrics.memoryTotal) * 100;
|
|
127
|
+
const bar = createProgressBar(memoryPercentage);
|
|
128
|
+
content += `Memory: {cyan-fg}${bar}{/cyan-fg} ${Math.round(memoryPercentage)}% `;
|
|
129
|
+
content += `(${memoryUsedGB.toFixed(1)} / ${memoryTotalGB.toFixed(1)} GB)\n`;
|
|
130
|
+
}
|
|
131
|
+
if (systemMetrics.warnings && systemMetrics.warnings.length > 0) {
|
|
132
|
+
content += `\n{yellow-fg}⚠ ${systemMetrics.warnings.join(', ')}{/yellow-fg}\n`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
content += '{gray-fg}Collecting system metrics...{/gray-fg}\n';
|
|
137
|
+
}
|
|
138
|
+
return content;
|
|
139
|
+
}
|
|
140
|
+
// Render aggregate model resources (all running servers in list view)
|
|
141
|
+
function renderAggregateModelResources() {
|
|
142
|
+
let content = '';
|
|
143
|
+
content += '{bold}Model Resources{/bold}\n';
|
|
144
|
+
const termWidth = screen.width || 80;
|
|
145
|
+
const divider = '─'.repeat(termWidth - 2);
|
|
146
|
+
content += divider + '\n';
|
|
147
|
+
// Aggregate CPU and memory across all running servers (skip stopped servers)
|
|
148
|
+
let totalCpu = 0;
|
|
149
|
+
let totalMemoryBytes = 0;
|
|
150
|
+
let serverCount = 0;
|
|
151
|
+
for (const serverData of serverDataMap.values()) {
|
|
152
|
+
// Only count running servers with valid data
|
|
153
|
+
if (serverData.server.status === 'running' && serverData.data?.server && !serverData.data.server.stale) {
|
|
154
|
+
if (serverData.data.server.processCpuUsage !== undefined) {
|
|
155
|
+
totalCpu += serverData.data.server.processCpuUsage;
|
|
156
|
+
serverCount++;
|
|
157
|
+
}
|
|
158
|
+
if (serverData.data.server.processMemory !== undefined) {
|
|
159
|
+
totalMemoryBytes += serverData.data.server.processMemory;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (serverCount === 0) {
|
|
164
|
+
content += '{gray-fg}No running servers{/gray-fg}\n';
|
|
165
|
+
return content;
|
|
166
|
+
}
|
|
167
|
+
// CPU: Sum of all process CPU percentages
|
|
168
|
+
const cpuBar = createProgressBar(Math.min(totalCpu, 100));
|
|
169
|
+
content += `CPU: {cyan-fg}${cpuBar}{/cyan-fg} ${Math.round(totalCpu)}%`;
|
|
170
|
+
content += ` {gray-fg}(${serverCount} ${serverCount === 1 ? 'server' : 'servers'}){/gray-fg}\n`;
|
|
171
|
+
// Memory: Sum of all process memory
|
|
172
|
+
const totalMemoryGB = totalMemoryBytes / (1024 ** 3);
|
|
173
|
+
const estimatedMaxGB = serverCount * 8; // Assume ~8GB per server max
|
|
174
|
+
const memoryPercentage = Math.min((totalMemoryGB / estimatedMaxGB) * 100, 100);
|
|
175
|
+
const memoryBar = createProgressBar(memoryPercentage);
|
|
176
|
+
content += `Memory: {cyan-fg}${memoryBar}{/cyan-fg} ${totalMemoryGB.toFixed(2)} GB`;
|
|
177
|
+
content += ` {gray-fg}(${serverCount} ${serverCount === 1 ? 'server' : 'servers'}){/gray-fg}\n`;
|
|
178
|
+
return content;
|
|
179
|
+
}
|
|
180
|
+
// Render model resources section (per-process for detail view)
|
|
181
|
+
function renderModelResources(data) {
|
|
182
|
+
let content = '';
|
|
183
|
+
content += '{bold}Model Resources{/bold}\n';
|
|
184
|
+
const termWidth = screen.width || 80;
|
|
185
|
+
const divider = '─'.repeat(termWidth - 2);
|
|
186
|
+
content += divider + '\n';
|
|
187
|
+
// GPU: System-wide (can't get per-process on macOS)
|
|
188
|
+
if (data.system && data.system.gpuUsage !== undefined) {
|
|
189
|
+
const bar = createProgressBar(data.system.gpuUsage);
|
|
190
|
+
content += `GPU: {cyan-fg}${bar}{/cyan-fg} ${Math.round(data.system.gpuUsage)}% {gray-fg}(system){/gray-fg}`;
|
|
191
|
+
if (data.system.temperature !== undefined) {
|
|
192
|
+
content += ` - ${Math.round(data.system.temperature)}°C`;
|
|
193
|
+
}
|
|
194
|
+
content += '\n';
|
|
195
|
+
}
|
|
196
|
+
// CPU: Per-process
|
|
197
|
+
if (data.server.processCpuUsage !== undefined) {
|
|
198
|
+
const bar = createProgressBar(data.server.processCpuUsage);
|
|
199
|
+
content += `CPU: {cyan-fg}${bar}{/cyan-fg} ${Math.round(data.server.processCpuUsage)}%\n`;
|
|
200
|
+
}
|
|
201
|
+
// Memory: Per-process
|
|
202
|
+
if (data.server.processMemory !== undefined) {
|
|
203
|
+
const memoryGB = data.server.processMemory / (1024 ** 3);
|
|
204
|
+
const estimatedMax = 8;
|
|
205
|
+
const memoryPercentage = Math.min((memoryGB / estimatedMax) * 100, 100);
|
|
206
|
+
const bar = createProgressBar(memoryPercentage);
|
|
207
|
+
content += `Memory: {cyan-fg}${bar}{/cyan-fg} ${memoryGB.toFixed(2)} GB\n`;
|
|
208
|
+
}
|
|
209
|
+
if (data.system && data.system.warnings && data.system.warnings.length > 0) {
|
|
210
|
+
content += `\n{yellow-fg}⚠ ${data.system.warnings.join(', ')}{/yellow-fg}\n`;
|
|
211
|
+
}
|
|
212
|
+
return content;
|
|
213
|
+
}
|
|
214
|
+
// Show loading spinner
|
|
215
|
+
function showLoading() {
|
|
216
|
+
if (isLoading)
|
|
217
|
+
return; // Already loading
|
|
218
|
+
isLoading = true;
|
|
219
|
+
spinnerFrameIndex = 0;
|
|
220
|
+
// Start spinner animation (80ms per frame = smooth rotation)
|
|
221
|
+
spinnerIntervalId = setInterval(() => {
|
|
222
|
+
spinnerFrameIndex = (spinnerFrameIndex + 1) % spinnerFrames.length;
|
|
223
|
+
// Re-render current view with updated spinner frame
|
|
224
|
+
let content = '';
|
|
225
|
+
if (viewMode === 'list') {
|
|
226
|
+
content = renderListView(lastSystemMetrics);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
content = renderDetailView(lastSystemMetrics);
|
|
230
|
+
}
|
|
231
|
+
contentBox.setContent(content);
|
|
232
|
+
screen.render();
|
|
233
|
+
}, 80);
|
|
234
|
+
// Immediate first render
|
|
235
|
+
let content = '';
|
|
236
|
+
if (viewMode === 'list') {
|
|
237
|
+
content = renderListView(lastSystemMetrics);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
content = renderDetailView(lastSystemMetrics);
|
|
241
|
+
}
|
|
242
|
+
contentBox.setContent(content);
|
|
243
|
+
screen.render();
|
|
244
|
+
}
|
|
245
|
+
// Hide loading spinner
|
|
246
|
+
function hideLoading() {
|
|
247
|
+
isLoading = false;
|
|
248
|
+
if (spinnerIntervalId) {
|
|
249
|
+
clearInterval(spinnerIntervalId);
|
|
250
|
+
spinnerIntervalId = null;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
// Render list view
|
|
254
|
+
function renderListView(systemMetrics) {
|
|
255
|
+
const termWidth = screen.width || 80;
|
|
256
|
+
const divider = '─'.repeat(termWidth - 2);
|
|
257
|
+
let content = '';
|
|
258
|
+
// Header
|
|
259
|
+
content += '{bold}{blue-fg}═══ llama.cpp{/blue-fg}{/bold}\n\n';
|
|
260
|
+
// System resources
|
|
261
|
+
content += renderSystemResources(systemMetrics);
|
|
262
|
+
content += '\n';
|
|
263
|
+
// Aggregate model resources (CPU + memory for all running servers)
|
|
264
|
+
content += renderAggregateModelResources();
|
|
265
|
+
content += '\n';
|
|
266
|
+
// Server list header
|
|
267
|
+
const runningCount = servers.filter(s => s.status === 'running').length;
|
|
268
|
+
const stoppedCount = servers.filter(s => s.status !== 'running').length;
|
|
269
|
+
content += `{bold}Servers (${runningCount} running, ${stoppedCount} stopped){/bold}\n`;
|
|
270
|
+
content += '{gray-fg}Use arrow keys to navigate, Enter to view details{/gray-fg}\n';
|
|
271
|
+
content += divider + '\n';
|
|
272
|
+
// Calculate Server ID column width (variable based on screen width)
|
|
273
|
+
// Fixed columns breakdown:
|
|
274
|
+
// indicator(1) + " │ "(3) + " │ "(3) + port(4) + " │ "(3) + status(6) + "│ "(2) +
|
|
275
|
+
// slots(5) + " │ "(3) + tok/s(6) + " │ "(3) + memory(7) = 46
|
|
276
|
+
const fixedColumnsWidth = 48; // Add 2 extra for safety margin
|
|
277
|
+
const minServerIdWidth = 20;
|
|
278
|
+
const maxServerIdWidth = 60;
|
|
279
|
+
const serverIdWidth = Math.max(minServerIdWidth, Math.min(maxServerIdWidth, termWidth - fixedColumnsWidth));
|
|
280
|
+
// Table header with variable Server ID width
|
|
281
|
+
const serverIdHeader = 'Server ID'.padEnd(serverIdWidth);
|
|
282
|
+
content += `{bold} │ ${serverIdHeader}│ Port │ Status │ Slots │ tok/s │ Memory{/bold}\n`;
|
|
283
|
+
content += divider + '\n';
|
|
284
|
+
// Server rows
|
|
285
|
+
servers.forEach((server, index) => {
|
|
286
|
+
const serverData = serverDataMap.get(server.id);
|
|
287
|
+
const isSelected = index === selectedRowIndex;
|
|
288
|
+
// Selection indicator (arrow for selected row)
|
|
289
|
+
// Use plain arrow for selected (will be white), colored for unselected indicator
|
|
290
|
+
const indicator = isSelected ? '►' : ' ';
|
|
291
|
+
// Server ID (variable width, truncate if longer than available space)
|
|
292
|
+
const serverId = server.id.padEnd(serverIdWidth).substring(0, serverIdWidth);
|
|
293
|
+
// Port
|
|
294
|
+
const port = server.port.toString().padStart(4);
|
|
295
|
+
// Status - Check actual server status first, then health
|
|
296
|
+
// Build two versions: colored for normal, plain for selected
|
|
297
|
+
let status = '';
|
|
298
|
+
let statusPlain = '';
|
|
299
|
+
if (server.status !== 'running') {
|
|
300
|
+
// Server is stopped according to config
|
|
301
|
+
status = '{gray-fg}○ OFF{/gray-fg} ';
|
|
302
|
+
statusPlain = '○ OFF ';
|
|
303
|
+
}
|
|
304
|
+
else if (serverData?.data) {
|
|
305
|
+
// Server is running and we have data
|
|
306
|
+
if (serverData.data.server.healthy) {
|
|
307
|
+
status = '{green-fg}● RUN{/green-fg} ';
|
|
308
|
+
statusPlain = '● RUN ';
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
status = '{red-fg}● ERR{/red-fg} ';
|
|
312
|
+
statusPlain = '● ERR ';
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
// Server is running but no data yet (still loading)
|
|
317
|
+
status = '{yellow-fg}● ...{/yellow-fg} ';
|
|
318
|
+
statusPlain = '● ... ';
|
|
319
|
+
}
|
|
320
|
+
// Slots
|
|
321
|
+
let slots = '- ';
|
|
322
|
+
if (serverData?.data?.server) {
|
|
323
|
+
const active = serverData.data.server.activeSlots;
|
|
324
|
+
const total = serverData.data.server.totalSlots;
|
|
325
|
+
slots = `${active}/${total}`.padStart(5);
|
|
326
|
+
}
|
|
327
|
+
// tok/s
|
|
328
|
+
let tokensPerSec = '- ';
|
|
329
|
+
if (serverData?.data?.server.avgGenerateSpeed !== undefined &&
|
|
330
|
+
serverData.data.server.avgGenerateSpeed > 0) {
|
|
331
|
+
tokensPerSec = Math.round(serverData.data.server.avgGenerateSpeed).toString().padStart(6);
|
|
332
|
+
}
|
|
333
|
+
// Memory (actual process memory from top command)
|
|
334
|
+
let memory = '- ';
|
|
335
|
+
if (serverData?.data?.server.processMemory) {
|
|
336
|
+
const bytes = serverData.data.server.processMemory;
|
|
337
|
+
// Format as GB/MB depending on size
|
|
338
|
+
if (bytes >= 1024 * 1024 * 1024) {
|
|
339
|
+
const gb = (bytes / (1024 * 1024 * 1024)).toFixed(1);
|
|
340
|
+
memory = `${gb} GB`.padStart(7);
|
|
341
|
+
}
|
|
342
|
+
else {
|
|
343
|
+
const mb = Math.round(bytes / (1024 * 1024));
|
|
344
|
+
memory = `${mb} MB`.padStart(7);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// Build row content - use plain status for selected rows
|
|
348
|
+
let rowContent = '';
|
|
349
|
+
if (isSelected) {
|
|
350
|
+
// Use color code 15 (bright white) with cyan background
|
|
351
|
+
// When white-bg worked, it was probably auto-selecting bright white fg
|
|
352
|
+
rowContent = `{cyan-bg}{15-fg}${indicator} │ ${serverId} │ ${port} │ ${statusPlain}│ ${slots} │ ${tokensPerSec} │ ${memory}{/15-fg}{/cyan-bg}`;
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
// Use colored status for normal rows
|
|
356
|
+
rowContent = `${indicator} │ ${serverId} │ ${port} │ ${status}│ ${slots} │ ${tokensPerSec} │ ${memory}`;
|
|
357
|
+
}
|
|
358
|
+
content += rowContent + '\n';
|
|
359
|
+
});
|
|
360
|
+
// Footer
|
|
361
|
+
content += '\n' + divider + '\n';
|
|
362
|
+
content += `{gray-fg}Updated: ${new Date().toLocaleTimeString()} | [H]istory [Q]uit{/gray-fg}`;
|
|
363
|
+
return content;
|
|
364
|
+
}
|
|
365
|
+
// Render detail view for selected server
|
|
366
|
+
function renderDetailView(systemMetrics) {
|
|
367
|
+
const server = servers[selectedServerIndex];
|
|
368
|
+
const serverData = serverDataMap.get(server.id);
|
|
369
|
+
const termWidth = screen.width || 80;
|
|
370
|
+
const divider = '─'.repeat(termWidth - 2);
|
|
371
|
+
let content = '';
|
|
372
|
+
// Header
|
|
373
|
+
content += `{bold}{blue-fg}═══ ${server.id} (${server.port}){/blue-fg}{/bold}\n\n`;
|
|
374
|
+
// Check if server is stopped
|
|
375
|
+
if (server.status !== 'running') {
|
|
376
|
+
// Show stopped server configuration (no metrics)
|
|
377
|
+
content += '{bold}Server Information{/bold}\n';
|
|
378
|
+
content += divider + '\n';
|
|
379
|
+
content += `Status: {gray-fg}○ STOPPED{/gray-fg}\n`;
|
|
380
|
+
content += `Model: ${server.modelName}\n`;
|
|
381
|
+
const displayHost = server.host || '127.0.0.1';
|
|
382
|
+
content += `Endpoint: http://${displayHost}:${server.port}\n`;
|
|
383
|
+
content += '\n';
|
|
384
|
+
content += '{bold}Configuration{/bold}\n';
|
|
385
|
+
content += divider + '\n';
|
|
386
|
+
content += `Threads: ${server.threads}\n`;
|
|
387
|
+
content += `Context: ${server.ctxSize} tokens\n`;
|
|
388
|
+
content += `GPU Layers: ${server.gpuLayers}\n`;
|
|
389
|
+
if (server.verbose) {
|
|
390
|
+
content += `Verbose: Enabled\n`;
|
|
391
|
+
}
|
|
392
|
+
if (server.customFlags && server.customFlags.length > 0) {
|
|
393
|
+
content += `Flags: ${server.customFlags.join(', ')}\n`;
|
|
394
|
+
}
|
|
395
|
+
content += '\n';
|
|
396
|
+
if (server.lastStarted) {
|
|
397
|
+
content += '{bold}Last Activity{/bold}\n';
|
|
398
|
+
content += divider + '\n';
|
|
399
|
+
content += `Started: ${new Date(server.lastStarted).toLocaleString()}\n`;
|
|
400
|
+
if (server.lastStopped) {
|
|
401
|
+
content += `Stopped: ${new Date(server.lastStopped).toLocaleString()}\n`;
|
|
402
|
+
}
|
|
403
|
+
content += '\n';
|
|
404
|
+
}
|
|
405
|
+
content += '{bold}Quick Actions{/bold}\n';
|
|
406
|
+
content += divider + '\n';
|
|
407
|
+
content += `{dim}Start server: llamacpp server start ${server.port}{/dim}\n`;
|
|
408
|
+
content += `{dim}Update config: llamacpp server config ${server.port} [options]{/dim}\n`;
|
|
409
|
+
content += `{dim}View logs: llamacpp server logs ${server.port}{/dim}\n`;
|
|
410
|
+
return content;
|
|
411
|
+
}
|
|
412
|
+
if (!serverData?.data) {
|
|
413
|
+
content += '{yellow-fg}Loading server data...{/yellow-fg}\n';
|
|
414
|
+
return content;
|
|
415
|
+
}
|
|
416
|
+
const data = serverData.data;
|
|
417
|
+
// Model resources (per-process)
|
|
418
|
+
content += renderModelResources(data);
|
|
419
|
+
content += '\n';
|
|
420
|
+
// Server Information
|
|
421
|
+
content += '{bold}Server Information{/bold}\n';
|
|
422
|
+
content += divider + '\n';
|
|
423
|
+
const statusIcon = data.server.healthy ? '{green-fg}●{/green-fg}' : '{red-fg}●{/red-fg}';
|
|
424
|
+
const statusText = data.server.healthy ? 'RUNNING' : 'UNHEALTHY';
|
|
425
|
+
content += `Status: ${statusIcon} ${statusText}`;
|
|
426
|
+
if (data.server.uptime) {
|
|
427
|
+
content += ` Uptime: ${data.server.uptime}`;
|
|
428
|
+
}
|
|
429
|
+
content += '\n';
|
|
430
|
+
content += `Model: ${server.modelName}`;
|
|
431
|
+
if (data.server.contextSize) {
|
|
432
|
+
content += ` Context: ${data.server.contextSize} tokens`;
|
|
433
|
+
}
|
|
434
|
+
content += '\n';
|
|
435
|
+
// Handle null host (legacy configs) by defaulting to 127.0.0.1
|
|
436
|
+
const displayHost = server.host || '127.0.0.1';
|
|
437
|
+
content += `Endpoint: http://${displayHost}:${server.port}\n`;
|
|
438
|
+
content += `Slots: ${data.server.activeSlots} active / ${data.server.totalSlots} total\n`;
|
|
439
|
+
content += '\n';
|
|
440
|
+
// Request Metrics
|
|
441
|
+
if (data.server.totalSlots > 0) {
|
|
442
|
+
content += '{bold}Request Metrics{/bold}\n';
|
|
443
|
+
content += divider + '\n';
|
|
444
|
+
content += `Active: ${data.server.activeSlots} / ${data.server.totalSlots}\n`;
|
|
445
|
+
content += `Idle: ${data.server.idleSlots} / ${data.server.totalSlots}\n`;
|
|
446
|
+
if (data.server.avgPromptSpeed !== undefined && data.server.avgPromptSpeed > 0) {
|
|
447
|
+
content += `Prompt: ${Math.round(data.server.avgPromptSpeed)} tokens/sec\n`;
|
|
448
|
+
}
|
|
449
|
+
if (data.server.avgGenerateSpeed !== undefined && data.server.avgGenerateSpeed > 0) {
|
|
450
|
+
content += `Generate: ${Math.round(data.server.avgGenerateSpeed)} tokens/sec\n`;
|
|
451
|
+
}
|
|
452
|
+
content += '\n';
|
|
453
|
+
}
|
|
454
|
+
// Active Slots Detail
|
|
455
|
+
if (data.server.slots.length > 0) {
|
|
456
|
+
const activeSlots = data.server.slots.filter(s => s.state === 'processing');
|
|
457
|
+
if (activeSlots.length > 0) {
|
|
458
|
+
content += '{bold}Active Slots{/bold}\n';
|
|
459
|
+
content += divider + '\n';
|
|
460
|
+
activeSlots.forEach((slot) => {
|
|
461
|
+
content += `Slot #${slot.id}: {yellow-fg}PROCESSING{/yellow-fg}`;
|
|
462
|
+
if (slot.timings?.predicted_per_second) {
|
|
463
|
+
content += ` - ${Math.round(slot.timings.predicted_per_second)} tok/s`;
|
|
464
|
+
}
|
|
465
|
+
if (slot.n_decoded !== undefined) {
|
|
466
|
+
content += ` - ${slot.n_decoded}`;
|
|
467
|
+
if (slot.n_ctx) {
|
|
468
|
+
content += ` / ${slot.n_ctx}`;
|
|
469
|
+
}
|
|
470
|
+
content += ' tokens';
|
|
471
|
+
}
|
|
472
|
+
content += '\n';
|
|
473
|
+
});
|
|
474
|
+
content += '\n';
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
// Footer
|
|
478
|
+
content += divider + '\n';
|
|
479
|
+
content += `{gray-fg}Updated: ${data.lastUpdated.toLocaleTimeString()} | [H]istory [ESC] Back [Q]uit{/gray-fg}`;
|
|
480
|
+
return content;
|
|
481
|
+
}
|
|
482
|
+
// Fetch and update display
|
|
483
|
+
async function fetchData() {
|
|
484
|
+
try {
|
|
485
|
+
// Collect system metrics ONCE for all servers (not per-server)
|
|
486
|
+
// This prevents spawning multiple macmon processes
|
|
487
|
+
const systemMetricsPromise = systemCollector.collectSystemMetrics();
|
|
488
|
+
// Batch collect process memory and CPU for ALL servers in parallel
|
|
489
|
+
// This prevents spawning multiple top processes (5x speedup)
|
|
490
|
+
const { getBatchProcessMemory, getBatchProcessCpu } = await Promise.resolve().then(() => __importStar(require('../utils/process-utils.js')));
|
|
491
|
+
const pids = servers.filter(s => s.pid).map(s => s.pid);
|
|
492
|
+
const memoryMapPromise = pids.length > 0
|
|
493
|
+
? getBatchProcessMemory(pids)
|
|
494
|
+
: Promise.resolve(new Map());
|
|
495
|
+
const cpuMapPromise = pids.length > 0
|
|
496
|
+
? getBatchProcessCpu(pids)
|
|
497
|
+
: Promise.resolve(new Map());
|
|
498
|
+
// Wait for both batches to complete
|
|
499
|
+
const [memoryMap, cpuMap] = await Promise.all([memoryMapPromise, cpuMapPromise]);
|
|
500
|
+
// Collect server metrics only for RUNNING servers (skip stopped servers)
|
|
501
|
+
const promises = servers
|
|
502
|
+
.filter(server => server.status === 'running')
|
|
503
|
+
.map(async (server) => {
|
|
504
|
+
const aggregator = aggregators.get(server.id);
|
|
505
|
+
try {
|
|
506
|
+
// Use collectServerMetrics instead of collectMonitorData
|
|
507
|
+
// to avoid spawning macmon per server
|
|
508
|
+
// Pass pre-fetched memory and CPU to avoid spawning top per server
|
|
509
|
+
const serverMetrics = await aggregator.collectServerMetrics(server, server.pid ? memoryMap.get(server.pid) ?? null : null, server.pid ? cpuMap.get(server.pid) ?? null : null);
|
|
510
|
+
// Build MonitorData manually with shared system metrics
|
|
511
|
+
const data = {
|
|
512
|
+
server: serverMetrics,
|
|
513
|
+
system: undefined, // Will be set after system metrics resolve
|
|
514
|
+
lastUpdated: new Date(),
|
|
515
|
+
updateInterval,
|
|
516
|
+
consecutiveFailures: 0,
|
|
517
|
+
};
|
|
518
|
+
serverDataMap.set(server.id, {
|
|
519
|
+
server,
|
|
520
|
+
data,
|
|
521
|
+
error: null,
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
catch (err) {
|
|
525
|
+
serverDataMap.set(server.id, {
|
|
526
|
+
server,
|
|
527
|
+
data: null,
|
|
528
|
+
error: err instanceof Error ? err.message : 'Unknown error',
|
|
529
|
+
});
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
// Set null data for stopped servers (no metrics collection)
|
|
533
|
+
servers
|
|
534
|
+
.filter(server => server.status !== 'running')
|
|
535
|
+
.forEach(server => {
|
|
536
|
+
serverDataMap.set(server.id, {
|
|
537
|
+
server,
|
|
538
|
+
data: null,
|
|
539
|
+
error: null,
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
// Wait for both system metrics and server metrics to complete
|
|
543
|
+
const systemMetrics = await systemMetricsPromise;
|
|
544
|
+
await Promise.all(promises);
|
|
545
|
+
// Store system metrics for loading state
|
|
546
|
+
lastSystemMetrics = systemMetrics;
|
|
547
|
+
// Update all server data with shared system metrics
|
|
548
|
+
for (const serverData of serverDataMap.values()) {
|
|
549
|
+
if (serverData.data) {
|
|
550
|
+
serverData.data.system = systemMetrics;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
// Append to history for each server (silent failure)
|
|
554
|
+
// Only save history for servers that are healthy and not stale
|
|
555
|
+
for (const [serverId, serverData] of serverDataMap) {
|
|
556
|
+
if (serverData.data && !serverData.data.server.stale && serverData.data.server.healthy) {
|
|
557
|
+
const manager = historyManagers.get(serverId);
|
|
558
|
+
manager?.appendSnapshot(serverData.data.server, serverData.data.system)
|
|
559
|
+
.catch(err => {
|
|
560
|
+
// Don't interrupt monitoring on history write failure
|
|
561
|
+
console.error(`Failed to save history for ${serverId}:`, err);
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
// Render once with complete data
|
|
566
|
+
let content = '';
|
|
567
|
+
if (viewMode === 'list') {
|
|
568
|
+
content = renderListView(systemMetrics);
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
content = renderDetailView(systemMetrics);
|
|
572
|
+
}
|
|
573
|
+
contentBox.setContent(content);
|
|
574
|
+
screen.render();
|
|
575
|
+
// Clear loading state
|
|
576
|
+
hideLoading();
|
|
577
|
+
}
|
|
578
|
+
catch (err) {
|
|
579
|
+
const errorMsg = err instanceof Error ? err.message : 'Unknown error';
|
|
580
|
+
contentBox.setContent('{bold}{red-fg}Error{/red-fg}{/bold}\n\n' +
|
|
581
|
+
`{red-fg}${errorMsg}{/red-fg}\n\n` +
|
|
582
|
+
'{gray-fg}Press [R] to retry or [Q] to quit{/gray-fg}');
|
|
583
|
+
screen.render();
|
|
584
|
+
// Clear loading state on error too
|
|
585
|
+
isLoading = false;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
// Polling
|
|
589
|
+
function startPolling() {
|
|
590
|
+
if (intervalId)
|
|
591
|
+
clearInterval(intervalId);
|
|
592
|
+
fetchData();
|
|
593
|
+
intervalId = setInterval(fetchData, updateInterval);
|
|
594
|
+
}
|
|
595
|
+
// Keyboard shortcuts - List view navigation with arrow keys
|
|
596
|
+
screen.key(['up', 'k'], () => {
|
|
597
|
+
if (viewMode === 'list') {
|
|
598
|
+
selectedRowIndex = Math.max(0, selectedRowIndex - 1);
|
|
599
|
+
// Re-render immediately for responsive feel
|
|
600
|
+
const content = renderListView(lastSystemMetrics);
|
|
601
|
+
contentBox.setContent(content);
|
|
602
|
+
screen.render();
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
screen.key(['down', 'j'], () => {
|
|
606
|
+
if (viewMode === 'list') {
|
|
607
|
+
selectedRowIndex = Math.min(servers.length - 1, selectedRowIndex + 1);
|
|
608
|
+
// Re-render immediately for responsive feel
|
|
609
|
+
const content = renderListView(lastSystemMetrics);
|
|
610
|
+
contentBox.setContent(content);
|
|
611
|
+
screen.render();
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
// Enter key to view details for selected server
|
|
615
|
+
screen.key(['enter'], () => {
|
|
616
|
+
if (viewMode === 'list') {
|
|
617
|
+
showLoading();
|
|
618
|
+
selectedServerIndex = selectedRowIndex;
|
|
619
|
+
viewMode = 'detail';
|
|
620
|
+
fetchData();
|
|
621
|
+
}
|
|
622
|
+
});
|
|
623
|
+
// Keyboard shortcuts - Detail view
|
|
624
|
+
screen.key(['escape'], () => {
|
|
625
|
+
// Don't handle ESC if we're in historical view - let historical view handle it
|
|
626
|
+
if (inHistoricalView)
|
|
627
|
+
return;
|
|
628
|
+
if (viewMode === 'detail') {
|
|
629
|
+
showLoading();
|
|
630
|
+
viewMode = 'list';
|
|
631
|
+
cameFromDirectJump = false; // Clear direct jump flag when returning to list
|
|
632
|
+
fetchData();
|
|
633
|
+
}
|
|
634
|
+
else if (viewMode === 'list') {
|
|
635
|
+
// ESC in list view - exit
|
|
636
|
+
showLoading();
|
|
637
|
+
if (intervalId)
|
|
638
|
+
clearInterval(intervalId);
|
|
639
|
+
if (spinnerIntervalId)
|
|
640
|
+
clearInterval(spinnerIntervalId);
|
|
641
|
+
setTimeout(() => {
|
|
642
|
+
screen.destroy();
|
|
643
|
+
process.exit(0);
|
|
644
|
+
}, 100);
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
// Keyboard shortcuts - Common
|
|
648
|
+
screen.key(['h', 'H'], async () => {
|
|
649
|
+
// Prevent entering historical view if already there
|
|
650
|
+
if (inHistoricalView)
|
|
651
|
+
return;
|
|
652
|
+
// Keep polling in background for live historical updates
|
|
653
|
+
// Stop spinner if running
|
|
654
|
+
if (spinnerIntervalId)
|
|
655
|
+
clearInterval(spinnerIntervalId);
|
|
656
|
+
// Remove current content box
|
|
657
|
+
screen.remove(contentBox);
|
|
658
|
+
// Mark that we're in historical view
|
|
659
|
+
inHistoricalView = true;
|
|
660
|
+
if (viewMode === 'list') {
|
|
661
|
+
// Show multi-server historical view
|
|
662
|
+
await (0, HistoricalMonitorApp_js_1.createMultiServerHistoricalUI)(screen, servers, selectedServerIndex, () => {
|
|
663
|
+
// Mark that we've left historical view
|
|
664
|
+
inHistoricalView = false;
|
|
665
|
+
// Re-attach content box when returning from history
|
|
666
|
+
screen.append(contentBox);
|
|
667
|
+
// Re-render the list view
|
|
668
|
+
const content = renderListView(lastSystemMetrics);
|
|
669
|
+
contentBox.setContent(content);
|
|
670
|
+
screen.render();
|
|
671
|
+
});
|
|
672
|
+
}
|
|
673
|
+
else {
|
|
674
|
+
// Show single-server historical view for selected server
|
|
675
|
+
const selectedServer = servers[selectedServerIndex];
|
|
676
|
+
await (0, HistoricalMonitorApp_js_1.createHistoricalUI)(screen, selectedServer, () => {
|
|
677
|
+
// Mark that we've left historical view
|
|
678
|
+
inHistoricalView = false;
|
|
679
|
+
// Re-attach content box when returning from history
|
|
680
|
+
screen.append(contentBox);
|
|
681
|
+
// Re-render the detail view
|
|
682
|
+
const content = renderDetailView(lastSystemMetrics);
|
|
683
|
+
contentBox.setContent(content);
|
|
684
|
+
screen.render();
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
});
|
|
688
|
+
screen.key(['q', 'Q', 'C-c'], () => {
|
|
689
|
+
showLoading();
|
|
690
|
+
if (intervalId)
|
|
691
|
+
clearInterval(intervalId);
|
|
692
|
+
if (spinnerIntervalId)
|
|
693
|
+
clearInterval(spinnerIntervalId);
|
|
694
|
+
// Small delay to show the loading state before exit
|
|
695
|
+
setTimeout(() => {
|
|
696
|
+
screen.destroy();
|
|
697
|
+
process.exit(0);
|
|
698
|
+
}, 100);
|
|
699
|
+
});
|
|
700
|
+
// Initial display
|
|
701
|
+
contentBox.setContent('{cyan-fg}⏳ Connecting to servers...{/cyan-fg}');
|
|
702
|
+
screen.render();
|
|
703
|
+
startPolling();
|
|
704
|
+
// Cleanup
|
|
705
|
+
screen.on('destroy', () => {
|
|
706
|
+
if (intervalId)
|
|
707
|
+
clearInterval(intervalId);
|
|
708
|
+
// Note: macmon child processes will automatically die when parent exits
|
|
709
|
+
// since they're spawned with detached: false
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
//# sourceMappingURL=MultiServerMonitorApp.js.map
|