@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.
Files changed (136) hide show
  1. package/README.md +294 -168
  2. package/dist/cli.js +35 -0
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/launch/claude.d.ts +6 -0
  5. package/dist/commands/launch/claude.d.ts.map +1 -0
  6. package/dist/commands/launch/claude.js +277 -0
  7. package/dist/commands/launch/claude.js.map +1 -0
  8. package/dist/lib/integration-checker.d.ts +26 -0
  9. package/dist/lib/integration-checker.d.ts.map +1 -0
  10. package/dist/lib/integration-checker.js +77 -0
  11. package/dist/lib/integration-checker.js.map +1 -0
  12. package/dist/lib/router-manager.d.ts +4 -0
  13. package/dist/lib/router-manager.d.ts.map +1 -1
  14. package/dist/lib/router-manager.js +10 -0
  15. package/dist/lib/router-manager.js.map +1 -1
  16. package/dist/lib/router-server.d.ts +13 -0
  17. package/dist/lib/router-server.d.ts.map +1 -1
  18. package/dist/lib/router-server.js +267 -7
  19. package/dist/lib/router-server.js.map +1 -1
  20. package/dist/types/integration-config.d.ts +28 -0
  21. package/dist/types/integration-config.d.ts.map +1 -0
  22. package/dist/types/integration-config.js +3 -0
  23. package/dist/types/integration-config.js.map +1 -0
  24. package/package.json +10 -2
  25. package/web/dist/assets/index-Bin89Lwr.css +1 -0
  26. package/web/dist/assets/index-CVmonw3T.js +17 -0
  27. package/web/{index.html → dist/index.html} +2 -1
  28. package/.versionrc.json +0 -16
  29. package/CHANGELOG.md +0 -213
  30. package/docs/images/.gitkeep +0 -1
  31. package/docs/images/web-ui-servers.png +0 -0
  32. package/src/cli.ts +0 -523
  33. package/src/commands/admin/config.ts +0 -121
  34. package/src/commands/admin/logs.ts +0 -91
  35. package/src/commands/admin/restart.ts +0 -26
  36. package/src/commands/admin/start.ts +0 -27
  37. package/src/commands/admin/status.ts +0 -84
  38. package/src/commands/admin/stop.ts +0 -16
  39. package/src/commands/config-global.ts +0 -38
  40. package/src/commands/config.ts +0 -323
  41. package/src/commands/create.ts +0 -183
  42. package/src/commands/delete.ts +0 -74
  43. package/src/commands/list.ts +0 -37
  44. package/src/commands/logs-all.ts +0 -251
  45. package/src/commands/logs.ts +0 -345
  46. package/src/commands/monitor.ts +0 -110
  47. package/src/commands/ps.ts +0 -84
  48. package/src/commands/pull.ts +0 -44
  49. package/src/commands/rm.ts +0 -107
  50. package/src/commands/router/config.ts +0 -116
  51. package/src/commands/router/logs.ts +0 -256
  52. package/src/commands/router/restart.ts +0 -36
  53. package/src/commands/router/start.ts +0 -60
  54. package/src/commands/router/status.ts +0 -119
  55. package/src/commands/router/stop.ts +0 -33
  56. package/src/commands/run.ts +0 -233
  57. package/src/commands/search.ts +0 -107
  58. package/src/commands/server-show.ts +0 -161
  59. package/src/commands/show.ts +0 -207
  60. package/src/commands/start.ts +0 -101
  61. package/src/commands/stop.ts +0 -39
  62. package/src/commands/tui.ts +0 -25
  63. package/src/lib/admin-manager.ts +0 -435
  64. package/src/lib/admin-server.ts +0 -1243
  65. package/src/lib/config-generator.ts +0 -130
  66. package/src/lib/download-job-manager.ts +0 -213
  67. package/src/lib/history-manager.ts +0 -172
  68. package/src/lib/launchctl-manager.ts +0 -225
  69. package/src/lib/metrics-aggregator.ts +0 -257
  70. package/src/lib/model-downloader.ts +0 -328
  71. package/src/lib/model-scanner.ts +0 -157
  72. package/src/lib/model-search.ts +0 -114
  73. package/src/lib/models-dir-setup.ts +0 -46
  74. package/src/lib/port-manager.ts +0 -80
  75. package/src/lib/router-logger.ts +0 -201
  76. package/src/lib/router-manager.ts +0 -414
  77. package/src/lib/router-server.ts +0 -538
  78. package/src/lib/state-manager.ts +0 -206
  79. package/src/lib/status-checker.ts +0 -113
  80. package/src/lib/system-collector.ts +0 -315
  81. package/src/tui/ConfigApp.ts +0 -1085
  82. package/src/tui/HistoricalMonitorApp.ts +0 -587
  83. package/src/tui/ModelsApp.ts +0 -368
  84. package/src/tui/MonitorApp.ts +0 -386
  85. package/src/tui/MultiServerMonitorApp.ts +0 -1833
  86. package/src/tui/RootNavigator.ts +0 -74
  87. package/src/tui/SearchApp.ts +0 -511
  88. package/src/tui/SplashScreen.ts +0 -149
  89. package/src/types/admin-config.ts +0 -25
  90. package/src/types/global-config.ts +0 -26
  91. package/src/types/history-types.ts +0 -39
  92. package/src/types/model-info.ts +0 -8
  93. package/src/types/monitor-types.ts +0 -162
  94. package/src/types/router-config.ts +0 -25
  95. package/src/types/server-config.ts +0 -46
  96. package/src/utils/downsample-utils.ts +0 -128
  97. package/src/utils/file-utils.ts +0 -146
  98. package/src/utils/format-utils.ts +0 -98
  99. package/src/utils/log-parser.ts +0 -284
  100. package/src/utils/log-utils.ts +0 -178
  101. package/src/utils/process-utils.ts +0 -316
  102. package/src/utils/prompt-utils.ts +0 -47
  103. package/test-load.sh +0 -100
  104. package/tsconfig.json +0 -20
  105. package/web/eslint.config.js +0 -23
  106. package/web/llamacpp-web-dist.tar.gz +0 -0
  107. package/web/package-lock.json +0 -4017
  108. package/web/package.json +0 -38
  109. package/web/postcss.config.js +0 -6
  110. package/web/src/App.css +0 -42
  111. package/web/src/App.tsx +0 -86
  112. package/web/src/assets/react.svg +0 -1
  113. package/web/src/components/ApiKeyPrompt.tsx +0 -71
  114. package/web/src/components/CreateServerModal.tsx +0 -372
  115. package/web/src/components/DownloadProgress.tsx +0 -123
  116. package/web/src/components/Nav.tsx +0 -89
  117. package/web/src/components/RouterConfigModal.tsx +0 -240
  118. package/web/src/components/SearchModal.tsx +0 -306
  119. package/web/src/components/ServerConfigModal.tsx +0 -291
  120. package/web/src/hooks/useApi.ts +0 -259
  121. package/web/src/index.css +0 -42
  122. package/web/src/lib/api.ts +0 -226
  123. package/web/src/main.tsx +0 -10
  124. package/web/src/pages/Dashboard.tsx +0 -103
  125. package/web/src/pages/Models.tsx +0 -258
  126. package/web/src/pages/Router.tsx +0 -270
  127. package/web/src/pages/RouterLogs.tsx +0 -201
  128. package/web/src/pages/ServerLogs.tsx +0 -553
  129. package/web/src/pages/Servers.tsx +0 -358
  130. package/web/src/types/api.ts +0 -140
  131. package/web/tailwind.config.js +0 -31
  132. package/web/tsconfig.app.json +0 -28
  133. package/web/tsconfig.json +0 -7
  134. package/web/tsconfig.node.json +0 -26
  135. package/web/vite.config.ts +0 -25
  136. /package/web/{public → dist}/vite.svg +0 -0
@@ -1,587 +0,0 @@
1
- import blessed from 'blessed';
2
- import * as asciichart from 'asciichart';
3
- import { ServerConfig } from '../types/server-config.js';
4
- import { HistoryManager } from '../lib/history-manager.js';
5
- import { HistorySnapshot } from '../types/history-types.js';
6
- import {
7
- downsampleMaxTimeWithFullHour,
8
- downsampleMeanTimeWithFullHour,
9
- getDownsampleRatio,
10
- TimeSeriesPoint
11
- } from '../utils/downsample-utils.js';
12
-
13
- type ViewMode = 'recent' | 'hour';
14
-
15
- // Shared view mode across both history screens - persists for the session
16
- let sharedViewMode: ViewMode = 'recent';
17
-
18
- interface ChartStats {
19
- avg: number;
20
- max: number;
21
- min: number;
22
- stddev: number;
23
- }
24
-
25
- interface ChartConfig {
26
- title: string;
27
- color: typeof asciichart.cyan;
28
- formatValue: (x: number) => string;
29
- isPercentage: boolean;
30
- noDataMessage: string;
31
- }
32
-
33
- /**
34
- * Calculate statistics for a set of values.
35
- */
36
- function calculateStats(values: number[]): ChartStats {
37
- if (values.length === 0) {
38
- return { avg: 0, max: 0, min: 0, stddev: 0 };
39
- }
40
-
41
- const avg = values.reduce((a, b) => a + b, 0) / values.length;
42
- const max = Math.max(...values);
43
- const min = Math.min(...values);
44
- const variance = values.reduce((sum, val) => sum + Math.pow(val - avg, 2), 0) / values.length;
45
- const stddev = Math.sqrt(variance);
46
-
47
- return { avg, max, min, stddev };
48
- }
49
-
50
- /**
51
- * Calculate expanded range for chart y-axis to prevent duplicate labels.
52
- */
53
- function getExpandedRange(data: number[], isPercentage: boolean): { min: number; max: number } {
54
- if (isPercentage) return { min: 0, max: 100 };
55
- if (data.length === 0) return { min: 0, max: 10 };
56
-
57
- const dataMin = Math.min(...data);
58
- const dataMax = Math.max(...data);
59
- const range = dataMax - dataMin;
60
- const padding = Math.max(range * 0.3, 5);
61
-
62
- return {
63
- min: Math.max(0, Math.floor(dataMin - padding)),
64
- max: Math.ceil(dataMax + padding)
65
- };
66
- }
67
-
68
- /**
69
- * Create a scrollable content box for historical charts.
70
- */
71
- function createContentBox(): blessed.Widgets.BoxElement {
72
- return blessed.box({
73
- top: 0,
74
- left: 0,
75
- width: '100%',
76
- height: '100%',
77
- tags: true,
78
- scrollable: true,
79
- alwaysScroll: true,
80
- keys: true,
81
- vi: true,
82
- mouse: true,
83
- scrollbar: {
84
- ch: '\u2588',
85
- style: { fg: 'blue' },
86
- },
87
- });
88
- }
89
-
90
- /**
91
- * Render a single chart with statistics.
92
- */
93
- function renderChart(
94
- values: number[],
95
- rawValues: TimeSeriesPoint[],
96
- config: ChartConfig,
97
- chartHeight: number
98
- ): string {
99
- let content = `{bold}${config.title}{/bold}\n`;
100
- const validValues = values.filter(v => !isNaN(v) && v > 0);
101
- const plotData = values.map(v => isNaN(v) ? 0 : v);
102
-
103
- try {
104
- if (validValues.length >= 2) {
105
- const range = getExpandedRange(validValues, config.isPercentage);
106
- content += asciichart.plot(plotData, {
107
- height: chartHeight,
108
- colors: [config.color],
109
- format: config.formatValue,
110
- min: range.min,
111
- max: range.max,
112
- });
113
- content += '\n';
114
-
115
- const stats = calculateStats(validValues);
116
- const lastValue = rawValues[rawValues.length - 1]?.value ?? 0;
117
-
118
- if (config.title.includes('GB')) {
119
- content += ` Avg: ${stats.avg.toFixed(2)} GB (\u00b1${stats.stddev.toFixed(2)}) `;
120
- content += `Max: ${stats.max.toFixed(2)} GB `;
121
- content += `Min: ${stats.min.toFixed(2)} GB `;
122
- content += `Last: ${lastValue.toFixed(2)} GB\n\n`;
123
- } else if (config.isPercentage) {
124
- content += ` Avg: ${stats.avg.toFixed(1)}% (\u00b1${stats.stddev.toFixed(1)}) `;
125
- content += `Max: ${stats.max.toFixed(1)}% `;
126
- content += `Min: ${stats.min.toFixed(1)}% `;
127
- content += `Last: ${lastValue.toFixed(1)}%\n\n`;
128
- } else {
129
- content += ` Avg: ${stats.avg.toFixed(1)} tok/s (\u00b1${stats.stddev.toFixed(1)}) `;
130
- content += `Max: ${stats.max.toFixed(1)} tok/s `;
131
- content += `Last: ${lastValue.toFixed(1)} tok/s\n\n`;
132
- }
133
- } else {
134
- const defaultRange = config.isPercentage ? { min: 0, max: 100 } : { min: 0, max: 10 };
135
- content += asciichart.plot(plotData, {
136
- height: chartHeight,
137
- colors: [config.color],
138
- format: config.formatValue,
139
- min: defaultRange.min,
140
- max: defaultRange.max,
141
- });
142
- content += `\n{gray-fg} ${config.noDataMessage}{/gray-fg}\n\n`;
143
- }
144
- } catch {
145
- content += '{red-fg} Error rendering chart{/red-fg}\n\n';
146
- }
147
-
148
- return content;
149
- }
150
-
151
- export async function createHistoricalUI(
152
- screen: blessed.Widgets.Screen,
153
- server: ServerConfig,
154
- onBack: () => void
155
- ): Promise<void> {
156
- const historyManager = new HistoryManager(server.id);
157
- let refreshIntervalId: NodeJS.Timeout | null = null;
158
- const REFRESH_INTERVAL = 1000;
159
- let lastGoodRender: string | null = null;
160
- let consecutiveErrors = 0;
161
-
162
- const contentBox = createContentBox();
163
- screen.append(contentBox);
164
-
165
- // Chart configurations
166
- const chartConfigs: Record<string, ChartConfig> = {
167
- tokenSpeed: {
168
- title: 'Server Token Generation Speed (tok/s)',
169
- color: asciichart.cyan,
170
- formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
171
- isPercentage: false,
172
- noDataMessage: 'No generation activity in this time window',
173
- },
174
- cpu: {
175
- title: 'Server CPU Usage (%)',
176
- color: asciichart.blue,
177
- formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
178
- isPercentage: true,
179
- noDataMessage: 'No CPU data in this time window',
180
- },
181
- memory: {
182
- title: 'Server Memory Usage (GB)',
183
- color: asciichart.magenta,
184
- formatValue: (x: number) => x.toFixed(2).padStart(6, ' '),
185
- isPercentage: false,
186
- noDataMessage: 'No memory data in this time window',
187
- },
188
- gpu: {
189
- title: 'System GPU Usage (%)',
190
- color: asciichart.green,
191
- formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
192
- isPercentage: true,
193
- noDataMessage: 'No GPU data in this time window',
194
- },
195
- };
196
-
197
- async function render(): Promise<void> {
198
- try {
199
- const termWidth = (screen.width as number) || 80;
200
- const divider = '\u2500'.repeat(termWidth - 2);
201
- const chartHeight = 5;
202
- const chartWidth = Math.min(Math.max(termWidth - 20, 40), 80);
203
-
204
- if (chartWidth <= 0 || !Number.isFinite(chartWidth)) {
205
- contentBox.setContent('{red-fg}Error: Invalid chart width{/red-fg}\n');
206
- screen.render();
207
- return;
208
- }
209
-
210
- // Header
211
- const modeLabel = sharedViewMode === 'recent' ? 'Minute' : 'Hour';
212
- const modeColor = sharedViewMode === 'recent' ? 'cyan' : 'magenta';
213
- let content = `{bold}{blue-fg}\u2550\u2550\u2550 ${server.modelName} (${server.port}) {/blue-fg} `;
214
- content += `{${modeColor}-fg}[${modeLabel}]{/${modeColor}-fg}{/bold}\n\n`;
215
-
216
- const snapshots = await historyManager.loadHistoryByWindow('1h');
217
-
218
- if (snapshots.length === 0) {
219
- content += '{yellow-fg}No historical data available.{/yellow-fg}\n\n';
220
- content += 'Historical data is collected when you run the monitor command.\n';
221
- content += 'Start monitoring to begin collecting history.\n\n';
222
- content += divider + '\n';
223
- content += '{gray-fg}[ESC] Back [Q]uit{/gray-fg}';
224
- contentBox.setContent(content);
225
- screen.render();
226
- return;
227
- }
228
-
229
- const maxChartPoints = Math.min(chartWidth, 80);
230
- const displaySnapshots = sharedViewMode === 'recent' && snapshots.length > maxChartPoints
231
- ? snapshots.slice(-maxChartPoints)
232
- : snapshots;
233
-
234
- // Extract time-series data
235
- const rawData = {
236
- tokenSpeed: [] as TimeSeriesPoint[],
237
- gpu: [] as TimeSeriesPoint[],
238
- cpu: [] as TimeSeriesPoint[],
239
- memory: [] as TimeSeriesPoint[],
240
- };
241
-
242
- for (const snapshot of displaySnapshots) {
243
- const ts = snapshot.timestamp;
244
- rawData.tokenSpeed.push({ timestamp: ts, value: snapshot.server.avgGenerateSpeed || 0 });
245
- rawData.gpu.push({ timestamp: ts, value: snapshot.system?.gpuUsage || 0 });
246
- rawData.cpu.push({ timestamp: ts, value: snapshot.server.processCpuUsage || 0 });
247
- rawData.memory.push({
248
- timestamp: ts,
249
- value: snapshot.server.processMemory ? snapshot.server.processMemory / (1024 ** 3) : 0
250
- });
251
- }
252
-
253
- // Apply downsampling based on view mode
254
- const useDownsampling = sharedViewMode === 'hour';
255
- const values = {
256
- tokenSpeed: useDownsampling
257
- ? downsampleMaxTimeWithFullHour(rawData.tokenSpeed, maxChartPoints)
258
- : rawData.tokenSpeed.map(p => p.value),
259
- cpu: useDownsampling
260
- ? downsampleMaxTimeWithFullHour(rawData.cpu, maxChartPoints)
261
- : rawData.cpu.map(p => p.value),
262
- memory: useDownsampling
263
- ? downsampleMeanTimeWithFullHour(rawData.memory, maxChartPoints)
264
- : rawData.memory.map(p => p.value),
265
- gpu: useDownsampling
266
- ? downsampleMaxTimeWithFullHour(rawData.gpu, maxChartPoints)
267
- : rawData.gpu.map(p => p.value),
268
- };
269
-
270
- // Render all charts
271
- content += renderChart(values.tokenSpeed, rawData.tokenSpeed, chartConfigs.tokenSpeed, chartHeight);
272
- content += renderChart(values.cpu, rawData.cpu, chartConfigs.cpu, chartHeight);
273
- content += renderChart(values.memory, rawData.memory, chartConfigs.memory, chartHeight);
274
- content += renderChart(values.gpu, rawData.gpu, chartConfigs.gpu, chartHeight);
275
-
276
- // Footer
277
- content += divider + '\n';
278
- content += `{gray-fg}[T]oggle Hour View [ESC] Back [Q]uit{/gray-fg}`;
279
-
280
- contentBox.setContent(content);
281
- screen.render();
282
-
283
- lastGoodRender = content;
284
- consecutiveErrors = 0;
285
- } catch (error) {
286
- consecutiveErrors++;
287
- if (lastGoodRender && consecutiveErrors < 5) {
288
- contentBox.setContent(lastGoodRender);
289
- } else {
290
- const errorMsg = error instanceof Error ? error.message : String(error);
291
- contentBox.setContent(
292
- '{bold}{red-fg}Render Error{/red-fg}{/bold}\n\n' +
293
- `{red-fg}${errorMsg}{/red-fg}\n\n` +
294
- `Consecutive errors: ${consecutiveErrors}\n\n` +
295
- '{gray-fg}[ESC] Back [Q]uit{/gray-fg}'
296
- );
297
- }
298
- screen.render();
299
- }
300
- }
301
-
302
- function cleanup(): void {
303
- if (refreshIntervalId) {
304
- clearInterval(refreshIntervalId);
305
- refreshIntervalId = null;
306
- }
307
- unregisterHandlers();
308
- }
309
-
310
- // Key handler functions (stored for unregistration)
311
- const keyHandlers = {
312
- toggle: () => {
313
- sharedViewMode = sharedViewMode === 'recent' ? 'hour' : 'recent';
314
- render();
315
- },
316
- escape: () => {
317
- cleanup();
318
- screen.remove(contentBox);
319
- onBack();
320
- },
321
- quit: () => {
322
- cleanup();
323
- screen.destroy();
324
- process.exit(0);
325
- },
326
- };
327
-
328
- function registerHandlers(): void {
329
- screen.key(['t', 'T'], keyHandlers.toggle);
330
- screen.key(['escape'], keyHandlers.escape);
331
- screen.key(['q', 'Q', 'C-c'], keyHandlers.quit);
332
- }
333
-
334
- function unregisterHandlers(): void {
335
- screen.unkey('t', keyHandlers.toggle);
336
- screen.unkey('T', keyHandlers.toggle);
337
- screen.unkey('escape', keyHandlers.escape);
338
- screen.unkey('q', keyHandlers.quit);
339
- screen.unkey('Q', keyHandlers.quit);
340
- screen.unkey('C-c', keyHandlers.quit);
341
- }
342
-
343
- registerHandlers();
344
-
345
- contentBox.setContent('{cyan-fg}\u23f3 Loading historical data...{/cyan-fg}');
346
- screen.render();
347
- await render();
348
-
349
- refreshIntervalId = setInterval(render, REFRESH_INTERVAL);
350
- }
351
-
352
- // Multi-server historical view
353
- export async function createMultiServerHistoricalUI(
354
- screen: blessed.Widgets.Screen,
355
- servers: ServerConfig[],
356
- _selectedIndex: number,
357
- onBack: () => void
358
- ): Promise<void> {
359
- let refreshIntervalId: NodeJS.Timeout | null = null;
360
- const REFRESH_INTERVAL = 3000;
361
- let lastGoodRender: string | null = null;
362
- let consecutiveErrors = 0;
363
-
364
- const contentBox = createContentBox();
365
- screen.append(contentBox);
366
-
367
- // Chart configurations for multi-server view
368
- const chartConfigs: Record<string, ChartConfig> = {
369
- tokenSpeed: {
370
- title: 'Total Server Token Generation Speed (tok/s)',
371
- color: asciichart.cyan,
372
- formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
373
- isPercentage: false,
374
- noDataMessage: 'No generation activity in this time window',
375
- },
376
- cpu: {
377
- title: 'Total Server CPU Usage (%)',
378
- color: asciichart.blue,
379
- formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
380
- isPercentage: true,
381
- noDataMessage: 'No CPU data in this time window',
382
- },
383
- memory: {
384
- title: 'Total Server Memory Usage (GB)',
385
- color: asciichart.magenta,
386
- formatValue: (x: number) => x.toFixed(2).padStart(6, ' '),
387
- isPercentage: false,
388
- noDataMessage: 'No memory data in this time window',
389
- },
390
- gpu: {
391
- title: 'System GPU Usage (%)',
392
- color: asciichart.green,
393
- formatValue: (x: number) => Math.round(x).toFixed(0).padStart(6, ' '),
394
- isPercentage: true,
395
- noDataMessage: 'No GPU data in this time window',
396
- },
397
- };
398
-
399
- async function render(): Promise<void> {
400
- try {
401
- const termWidth = (screen.width as number) || 80;
402
- const divider = '\u2500'.repeat(termWidth - 2);
403
- const chartWidth = Math.min(Math.max(40, termWidth - 20), 80);
404
- const chartHeight = 5;
405
-
406
- // Header
407
- const modeLabel = sharedViewMode === 'recent' ? 'Minute' : 'Hour';
408
- const modeColor = sharedViewMode === 'recent' ? 'cyan' : 'magenta';
409
- let content = `{bold}{blue-fg}\u2550\u2550\u2550 All servers (${servers.length}){/blue-fg} `;
410
- content += `{${modeColor}-fg}[${modeLabel}]{/${modeColor}-fg}{/bold}\n\n`;
411
-
412
- // Load and aggregate history for all servers
413
- const serverHistories = await Promise.all(
414
- servers.map(async (server) => {
415
- const manager = new HistoryManager(server.id);
416
- return manager.loadHistoryByWindow('1h');
417
- })
418
- );
419
-
420
- // Aggregate data across all servers at each timestamp
421
- const ALIGNMENT_INTERVAL = 2000;
422
- const timestampMap = new Map<number, {
423
- tokensPerSec: number[];
424
- gpuUsage: number[];
425
- cpuUsage: number[];
426
- memoryGB: number[];
427
- }>();
428
-
429
- for (const snapshots of serverHistories) {
430
- for (const snapshot of snapshots) {
431
- const timestamp = Math.round(snapshot.timestamp / ALIGNMENT_INTERVAL) * ALIGNMENT_INTERVAL;
432
-
433
- if (!timestampMap.has(timestamp)) {
434
- timestampMap.set(timestamp, {
435
- tokensPerSec: [],
436
- gpuUsage: [],
437
- cpuUsage: [],
438
- memoryGB: [],
439
- });
440
- }
441
-
442
- const data = timestampMap.get(timestamp)!;
443
-
444
- if (snapshot.server.avgGenerateSpeed && snapshot.server.avgGenerateSpeed > 0) {
445
- data.tokensPerSec.push(snapshot.server.avgGenerateSpeed);
446
- }
447
- if (snapshot.system?.gpuUsage !== undefined) {
448
- data.gpuUsage.push(snapshot.system.gpuUsage);
449
- }
450
- if (snapshot.server.processCpuUsage !== undefined) {
451
- data.cpuUsage.push(snapshot.server.processCpuUsage);
452
- }
453
- if (snapshot.server.processMemory) {
454
- data.memoryGB.push(snapshot.server.processMemory / (1024 ** 3));
455
- }
456
- }
457
- }
458
-
459
- // Sort timestamps and aggregate
460
- const timestamps = Array.from(timestampMap.keys()).sort((a, b) => a - b);
461
- const aggregatedData = timestamps.map(ts => {
462
- const data = timestampMap.get(ts)!;
463
- return {
464
- timestamp: ts,
465
- totalTokS: data.tokensPerSec.reduce((a, b) => a + b, 0),
466
- avgGpu: data.gpuUsage.length > 0
467
- ? data.gpuUsage.reduce((a, b) => a + b, 0) / data.gpuUsage.length
468
- : 0,
469
- totalCpu: data.cpuUsage.reduce((a, b) => a + b, 0),
470
- totalMemoryGB: data.memoryGB.reduce((a, b) => a + b, 0),
471
- };
472
- });
473
-
474
- if (aggregatedData.length > 0) {
475
- const maxPoints = Math.min(chartWidth, 80);
476
- const displayData = sharedViewMode === 'recent' && aggregatedData.length > maxPoints
477
- ? aggregatedData.slice(-maxPoints)
478
- : aggregatedData;
479
-
480
- const useDownsampling = sharedViewMode === 'hour';
481
-
482
- // Extract time-series data
483
- const rawData = {
484
- tokenSpeed: displayData.map(d => ({ timestamp: d.timestamp, value: d.totalTokS })),
485
- cpu: displayData.map(d => ({ timestamp: d.timestamp, value: d.totalCpu })),
486
- memory: displayData.map(d => ({ timestamp: d.timestamp, value: d.totalMemoryGB })),
487
- gpu: displayData.map(d => ({ timestamp: d.timestamp, value: d.avgGpu })),
488
- };
489
-
490
- // Apply downsampling
491
- const values = {
492
- tokenSpeed: useDownsampling
493
- ? downsampleMaxTimeWithFullHour(rawData.tokenSpeed, chartWidth)
494
- : rawData.tokenSpeed.map(d => d.value),
495
- cpu: useDownsampling
496
- ? downsampleMaxTimeWithFullHour(rawData.cpu, chartWidth)
497
- : rawData.cpu.map(d => d.value),
498
- memory: useDownsampling
499
- ? downsampleMeanTimeWithFullHour(rawData.memory, chartWidth)
500
- : rawData.memory.map(d => d.value),
501
- gpu: useDownsampling
502
- ? downsampleMaxTimeWithFullHour(rawData.gpu, chartWidth)
503
- : rawData.gpu.map(d => d.value),
504
- };
505
-
506
- // Render all charts
507
- content += renderChart(values.tokenSpeed, rawData.tokenSpeed, chartConfigs.tokenSpeed, chartHeight);
508
- content += renderChart(values.cpu, rawData.cpu, chartConfigs.cpu, chartHeight);
509
- content += renderChart(values.memory, rawData.memory, chartConfigs.memory, chartHeight);
510
- content += renderChart(values.gpu, rawData.gpu, chartConfigs.gpu, chartHeight);
511
- }
512
-
513
- // Footer
514
- content += divider + '\n';
515
- content += `{gray-fg}[T]oggle Hour View [ESC] Back [Q]uit{/gray-fg}`;
516
-
517
- contentBox.setContent(content);
518
- screen.render();
519
-
520
- lastGoodRender = content;
521
- consecutiveErrors = 0;
522
- } catch (error) {
523
- consecutiveErrors++;
524
- if (lastGoodRender && consecutiveErrors < 5) {
525
- contentBox.setContent(lastGoodRender);
526
- } else {
527
- const errorMsg = error instanceof Error ? error.message : String(error);
528
- contentBox.setContent(
529
- '{bold}{red-fg}Render Error{/red-fg}{/bold}\n\n' +
530
- `{red-fg}${errorMsg}{/red-fg}\n\n` +
531
- `Consecutive errors: ${consecutiveErrors}\n\n` +
532
- '{gray-fg}[ESC] Back [Q]uit{/gray-fg}'
533
- );
534
- }
535
- screen.render();
536
- }
537
- }
538
-
539
- function cleanup(): void {
540
- if (refreshIntervalId) {
541
- clearInterval(refreshIntervalId);
542
- refreshIntervalId = null;
543
- }
544
- unregisterHandlers();
545
- }
546
-
547
- // Key handler functions (stored for unregistration)
548
- const keyHandlers = {
549
- toggle: () => {
550
- sharedViewMode = sharedViewMode === 'recent' ? 'hour' : 'recent';
551
- render();
552
- },
553
- escape: () => {
554
- cleanup();
555
- screen.remove(contentBox);
556
- onBack();
557
- },
558
- quit: () => {
559
- cleanup();
560
- screen.destroy();
561
- process.exit(0);
562
- },
563
- };
564
-
565
- function registerHandlers(): void {
566
- screen.key(['t', 'T'], keyHandlers.toggle);
567
- screen.key(['escape'], keyHandlers.escape);
568
- screen.key(['q', 'Q', 'C-c'], keyHandlers.quit);
569
- }
570
-
571
- function unregisterHandlers(): void {
572
- screen.unkey('t', keyHandlers.toggle);
573
- screen.unkey('T', keyHandlers.toggle);
574
- screen.unkey('escape', keyHandlers.escape);
575
- screen.unkey('q', keyHandlers.quit);
576
- screen.unkey('Q', keyHandlers.quit);
577
- screen.unkey('C-c', keyHandlers.quit);
578
- }
579
-
580
- registerHandlers();
581
-
582
- contentBox.setContent('{cyan-fg}\u23f3 Loading historical data...{/cyan-fg}');
583
- screen.render();
584
- await render();
585
-
586
- refreshIntervalId = setInterval(render, REFRESH_INTERVAL);
587
- }