@pixlcore/xycli 1.0.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 (63) hide show
  1. package/README.md +543 -0
  2. package/docs/help.md +2587 -0
  3. package/docs/screenshots.md +219 -0
  4. package/docs/sync.md +920 -0
  5. package/docs/tutorial.md +204 -0
  6. package/index.js +444 -0
  7. package/lib/alerts.js +735 -0
  8. package/lib/apikey.js +425 -0
  9. package/lib/buckets.js +607 -0
  10. package/lib/categories.js +323 -0
  11. package/lib/channels.js +290 -0
  12. package/lib/config.js +86 -0
  13. package/lib/dashboard.js +237 -0
  14. package/lib/doc.js +131 -0
  15. package/lib/events.js +1314 -0
  16. package/lib/groups.js +1038 -0
  17. package/lib/jobs.js +1254 -0
  18. package/lib/log.js +125 -0
  19. package/lib/marketplace.js +386 -0
  20. package/lib/monitors.js +412 -0
  21. package/lib/plugins.js +491 -0
  22. package/lib/secrets.js +441 -0
  23. package/lib/servers.js +1502 -0
  24. package/lib/snapshots.js +498 -0
  25. package/lib/sync.js +963 -0
  26. package/lib/system.js +612 -0
  27. package/lib/tags.js +250 -0
  28. package/lib/tickets.js +681 -0
  29. package/lib/transfer.js +447 -0
  30. package/lib/utils.js +2745 -0
  31. package/lib/webhook.js +414 -0
  32. package/logo.webp +0 -0
  33. package/package.json +38 -0
  34. package/test/README.md +97 -0
  35. package/test/api-validation.test.js +170 -0
  36. package/test/categories.test.js +246 -0
  37. package/test/channels.test.js +209 -0
  38. package/test/config.test.js +96 -0
  39. package/test/doc.test.js +84 -0
  40. package/test/groups.test.js +211 -0
  41. package/test/help.test.js +26 -0
  42. package/test/helpers/common.js +127 -0
  43. package/test/helpers/pagination-preload.cjs +18 -0
  44. package/test/log.test.js +103 -0
  45. package/test/markdown.test.js +61 -0
  46. package/test/marketplace.test.js +229 -0
  47. package/test/monitors.test.js +227 -0
  48. package/test/pagination.test.js +138 -0
  49. package/test/plugins.test.js +351 -0
  50. package/test/secrets.test.js +271 -0
  51. package/test/servers.test.js +303 -0
  52. package/test/snapshots.test.js +167 -0
  53. package/test/sync-unit.test.js +446 -0
  54. package/test/sync.test.js +542 -0
  55. package/test/system.test.js +94 -0
  56. package/test/tags.test.js +230 -0
  57. package/test/tickets.test.js +261 -0
  58. package/test/toast.test.js +73 -0
  59. package/test/transfer-apikey.test.js +96 -0
  60. package/test/transfer-workflow.test.js +122 -0
  61. package/test/transfer.test.js +314 -0
  62. package/test/webhook.test.js +332 -0
  63. package/test.js +49 -0
package/lib/servers.js ADDED
@@ -0,0 +1,1502 @@
1
+ // Server Management Layer
2
+
3
+ const cli = require('pixl-cli');
4
+ const Tools = cli.Tools;
5
+
6
+ const SERVER_PLATFORMS = {
7
+ linux: 'standard',
8
+ macos: 'macos',
9
+ windows: 'windows',
10
+ docker: 'docker'
11
+ };
12
+
13
+ const SERVER_LIST_FILTERS = [
14
+ 'id', 'title', 'hostname', 'ip', 'os', 'platform', 'os_platform',
15
+ 'distro', 'os_distro', 'release', 'os_release', 'arch', 'os_arch',
16
+ 'cpu', 'satellite', 'group', 'groups', 'status', 'online', 'enabled'
17
+ ];
18
+
19
+ const SERVER_SEARCH_FIELDS = [
20
+ 'groups', 'os_platform', 'os_distro', 'os_release', 'os_arch',
21
+ 'cpu_virt', 'cpu_brand', 'cpu_cores', 'created', 'modified'
22
+ ];
23
+
24
+ // Server records use a few camelCase properties internally, but the public CLI
25
+ // favors readable snake_case switches and the friendlier "label" terminology.
26
+ const SERVER_UPDATE_ALIASES = {
27
+ label: 'title',
28
+ title: 'title',
29
+ enabled: 'enabled',
30
+ icon: 'icon',
31
+ groups: 'groups',
32
+ max_jobs: 'maxJobs',
33
+ maxJobs: 'maxJobs',
34
+ user_data: 'userData',
35
+ userData: 'userData'
36
+ };
37
+
38
+ const SERVER_HISTORY_SYSTEMS = {
39
+ hourly: { parts: 4, epoch_div: 60 },
40
+ daily: { parts: 3, epoch_div: 120 },
41
+ monthly: { parts: 2, epoch_div: 3600 },
42
+ yearly: { parts: 1, epoch_div: 43200 }
43
+ };
44
+
45
+ module.exports = {
46
+
47
+ async cmd_servers() {
48
+ // The plural form normally shows live and recently offline servers. Keep
49
+ // the database-search spelling available here as a convenient alias.
50
+ if (this.args.other[0] == 'search') {
51
+ this.args.other.shift();
52
+ return this.cmd_search_servers();
53
+ }
54
+ await this.cmd_get_servers();
55
+ },
56
+
57
+ async cmd_server() {
58
+ // Most Server commands use a named subcommand, while the common detail
59
+ // view accepts a Server ID directly for convenient day-to-day use.
60
+ var cmd = this.args.other.shift();
61
+ if (!cmd) return this.dieUsage('server');
62
+
63
+ switch (cmd) {
64
+ case 'list': await this.cmd_get_servers(); break;
65
+ case 'get': await this.cmd_get_server(); break;
66
+ case 'add': await this.cmd_add_server(); break;
67
+ case 'search': await this.cmd_search_servers(); break;
68
+ case 'history': await this.cmd_server_history(); break;
69
+ default:
70
+ this.args.other.unshift(cmd);
71
+ await this.cmd_get_server();
72
+ break;
73
+ }
74
+ },
75
+
76
+ async cmd_get_server() {
77
+ // Fetch the Server definition and its latest monitoring snapshot. The
78
+ // heavier timelines are opt-in, while Quick Look is live-only.
79
+ this.prepSearchArgs();
80
+ var selector = this.args.other.shift();
81
+ if (!selector || this.args.other.length) return this.dieUsage('server get');
82
+ delete this.args.other;
83
+
84
+ var command = this.getServerCommand();
85
+ var show_monitors = false;
86
+ var show_processes = false;
87
+ var show_connections = false;
88
+ var pid;
89
+
90
+ if (!command) {
91
+ show_monitors = this.verbose || !!this.args.monitors;
92
+ show_processes = this.verbose || !!this.args.processes;
93
+ show_connections = this.verbose || !!this.args.connections;
94
+ pid = this.args.pid;
95
+
96
+ if ((pid !== undefined) && !String(pid).match(/^\d+$/)) this.die('Server process ID must be a positive integer.');
97
+ if ((pid !== undefined) && (Number(pid) < 1)) this.die('Server process ID must be a positive integer.');
98
+ if (('monitors' in this.args) && (typeof(this.args.monitors) != 'boolean')) this.die('Server --monitors must be true or false.');
99
+ if (('processes' in this.args) && (typeof(this.args.processes) != 'boolean')) this.die('Server --processes must be true or false.');
100
+ if (('connections' in this.args) && (typeof(this.args.connections) != 'boolean')) this.die('Server --connections must be true or false.');
101
+
102
+ delete this.args.monitors;
103
+ delete this.args.processes;
104
+ delete this.args.connections;
105
+ delete this.args.pid;
106
+ if (Tools.numKeys(this.args)) {
107
+ var key = Tools.firstKey(this.args);
108
+ var choices = ['monitors', 'processes', 'connections', 'pid', 'snapshot', 'watch', 'delete'].concat(Object.keys(SERVER_UPDATE_ALIASES));
109
+ var suggestion = this.findClosestString(key, choices);
110
+ this.die('Unsupported Server view option: "--' + key + '".' + (suggestion ? ' Did you mean "--' + suggestion + '"?' : ''));
111
+ }
112
+ }
113
+
114
+ await this.getMultiple();
115
+ var local_server = this.findLocalServer(selector);
116
+ var id = local_server ? local_server.id : selector;
117
+
118
+ cli.progress.start({ amount: 1, pct: false, text: gray('\u2192 Loading server...') });
119
+ var { err, data: response } = await this.api.getServer({ id: id });
120
+ cli.progress.end();
121
+ if (err) this.die(err);
122
+
123
+ var server = response.server;
124
+ var snapshot = response.data || {};
125
+ var online = !!response.online;
126
+ var live_data = snapshot.data || {};
127
+
128
+ // Mutating commands share Server selection and loading with the detail view,
129
+ // but stop here so no dashboard sections are printed after the operation.
130
+ if (command) return this.runServerCommand(command, server, online);
131
+
132
+ // Process inspection is deliberately its own focused page. Do not fetch
133
+ // timelines or render the much larger general Server view around it.
134
+ if (pid !== undefined) {
135
+ var detail = this.getServerProcessDetailData(live_data, Number(pid));
136
+ if (this.format.match(/json/)) {
137
+ return this.jsonOutput({
138
+ server: { id: server.id, hostname: server.hostname, title: server.title },
139
+ process: detail.process,
140
+ family: detail.family
141
+ });
142
+ }
143
+ this.printServerProcessDetail(server, live_data, Number(pid), detail);
144
+ return;
145
+ }
146
+
147
+ var supports_quickmon = !!(online && server && server.info && server.info.quickmon && !live_data.dummy);
148
+ var quickmon = [];
149
+ var monitor_rows = [];
150
+ var requests = [];
151
+
152
+ if (supports_quickmon) {
153
+ requests.push( this.api.getQuickmonData({ server: server.id }).then( result => {
154
+ if (result.err) throw result.err;
155
+ quickmon = (result.data.servers && result.data.servers[server.id]) || [];
156
+ }) );
157
+ }
158
+ if (show_monitors && !live_data.dummy) {
159
+ requests.push( this.api.getLatestMonitorData({ server: server.id, sys: 'hourly', limit: 60 }).then( result => {
160
+ if (result.err) throw result.err;
161
+ // The current interval may be represented by a sparse placeholder
162
+ // while xySat is still assembling it. Charts only consume complete rows.
163
+ monitor_rows = (result.data.rows || []).filter( row => row && row.date && row.totals );
164
+ }) );
165
+ }
166
+
167
+ if (requests.length) {
168
+ cli.progress.start({ amount: 1, pct: false, text: gray('\u2192 Loading server monitoring data...') });
169
+ try { await Promise.all(requests); }
170
+ catch (error) { cli.progress.end(); this.die(error); }
171
+ cli.progress.end();
172
+ }
173
+
174
+ var result = {
175
+ server: server,
176
+ data: snapshot,
177
+ online: online,
178
+ quickmon: quickmon,
179
+ monitors: monitor_rows
180
+ };
181
+ if (this.format.match(/json/)) return this.jsonOutput(result);
182
+
183
+ // Give the normal Server view a clear page-level identity above its sections.
184
+ if (online) println('\n ' + green.bold('LIVE SERVER VIEW - Real-time'));
185
+ else println('\n ' + yellow.bold('OFFLINE SERVER - Last Known State'));
186
+
187
+ this.printServerSummary(server, snapshot, online);
188
+ this.printServerAlerts(server, snapshot, online);
189
+ if (online) this.printServerJobs(server);
190
+ if (supports_quickmon && quickmon.length) this.printServerQuickmonCharts(server, quickmon);
191
+
192
+ if (!live_data.dummy) {
193
+ this.printServerMemory(live_data);
194
+ this.printServerCPU(live_data);
195
+ this.printServerMonitorGrid(server, live_data);
196
+
197
+ if (show_monitors) this.printServerMonitorCharts(server, monitor_rows, online);
198
+ else this.printServerExpansionNotice('Server Monitors', '--monitors');
199
+
200
+ this.printServerContainers(live_data);
201
+
202
+ if (show_processes) this.printServerProcesses(live_data);
203
+ else this.printServerExpansionNotice('Server Processes', '--processes');
204
+
205
+ if (show_connections) this.printServerConnections(live_data);
206
+ else this.printServerExpansionNotice('Network Connections', '--connections');
207
+
208
+ this.printServerInterfaces(live_data);
209
+ this.printServerFilesystems(live_data);
210
+ }
211
+
212
+ this.printSuggestedCommands({
213
+ 'Show server monitors': show_monitors ? '' : 'xy server ' + server.id + ' --monitors',
214
+ 'Show server processes': show_processes ? '' : 'xy server ' + server.id + ' --processes',
215
+ 'Show process details': 'xy server ' + server.id + ' --pid 1234',
216
+ 'Show network connections': show_connections ? '' : 'xy server ' + server.id + ' --connections',
217
+ 'Show all server details': this.verbose ? '' : 'xy server ' + server.id + ' --verbose',
218
+ 'Edit server label': 'xy server ' + server.id + ' --label "New Label"',
219
+ 'Take a server snapshot': online ? 'xy server ' + server.id + ' --snapshot' : '',
220
+ 'Set a five-minute watch': online ? 'xy server ' + server.id + ' --watch 300' : '',
221
+ 'Delete this server': 'xy server ' + server.id + ' --delete',
222
+ 'Search server history': 'xy server search ' + server.hostname
223
+ });
224
+ },
225
+
226
+ getServerCommand() {
227
+ // Action switches are mutually exclusive, and editing cannot be combined
228
+ // with a snapshot, watch or delete in the same invocation.
229
+ var actions = ['snapshot', 'watch', 'delete'].filter( key => key in this.args );
230
+ var edit_keys = Object.keys(this.args).filter( key => key in SERVER_UPDATE_ALIASES );
231
+ if (actions.length > 1) this.die('Specify only one server action at a time: --' + actions.join(', --') + '.');
232
+ if (actions.length && edit_keys.length) this.die('Server edits cannot be combined with --' + actions[0] + '.');
233
+ return actions[0] || (edit_keys.length ? 'update' : '');
234
+ },
235
+
236
+ async runServerCommand(command, server, online) {
237
+ switch (command) {
238
+ case 'update': return this.updateServerFromCLI(server, online);
239
+ case 'snapshot': return this.createServerSnapshotFromCLI(server, online);
240
+ case 'watch': return this.watchServerFromCLI(server, online);
241
+ case 'delete': return this.deleteServerFromCLI(server, online);
242
+ }
243
+ },
244
+
245
+ getServerCommandSummaryRows(server, online) {
246
+ // Keep the target visible before every mutation, especially when a hostname
247
+ // or custom label was used to select the Server instead of its internal ID.
248
+ return [
249
+ ['Server ID', this.color('theme').bold(server.id)],
250
+ ['Hostname', server.hostname || gray('(Unknown)')],
251
+ server.title ? ['Label', bold(server.title)] : null,
252
+ ['Status', online ? green('Online') : gray('Offline')]
253
+ ];
254
+ },
255
+
256
+ async updateServerFromCLI(server, online) {
257
+ this.printMutationSummary({
258
+ title: 'Update Server',
259
+ rows: this.getServerCommandSummaryRows(server, online)
260
+ });
261
+ if (!Tools.numKeys(this.args)) return this.die('No updates specified for server.');
262
+ this.printUpdateData(this.args);
263
+
264
+ var params = { id: server.id };
265
+ Object.keys(this.args).forEach( key => {
266
+ var api_key = SERVER_UPDATE_ALIASES[key];
267
+ if (!api_key) {
268
+ var suggestion = this.findClosestString(key, Object.keys(SERVER_UPDATE_ALIASES));
269
+ this.die('Unsupported Server update option: "--' + key + '".' + (suggestion ? ' Did you mean "--' + suggestion + '"?' : ''));
270
+ }
271
+ if (api_key in params) this.die('Duplicate server update options for: ' + api_key + '.');
272
+ params[api_key] = this.args[key];
273
+ });
274
+
275
+ if ('title' in params) {
276
+ if (typeof(params.title) != 'string') this.die('Server label must be text.');
277
+ params.title = params.title.trim();
278
+ }
279
+ if ('enabled' in params) params.enabled = this.parseServerBoolean(params.enabled, 'enabled');
280
+ if ('icon' in params) {
281
+ if (typeof(params.icon) != 'string') this.die('Server icon must be text.');
282
+ params.icon = params.icon.trim().replace(/^mdi\-/, '');
283
+ }
284
+ if ('groups' in params) {
285
+ params.groups = this.parseServerList(params.groups, 'groups');
286
+ params.groups.forEach( id => {
287
+ if (Tools.findObject(this.groups, { id: id })) return;
288
+ var suggestion = this.findClosestString(id, this.groups.map( group => group.id ));
289
+ this.die('Unknown server Group ID: "' + id + '".' + (suggestion ? ' Did you mean "' + suggestion + '"?' : ''));
290
+ });
291
+ params.autoGroup = !params.groups.length;
292
+ }
293
+ if ('maxJobs' in params) {
294
+ if (!Number.isInteger(params.maxJobs) || (params.maxJobs < 0)) this.die('Server --max_jobs must be a non-negative integer.');
295
+ }
296
+ if (('userData' in params) && !Tools.isaHash(params.userData)) this.die('Server --user_data must be a JSON object.');
297
+
298
+ var data = await this.callStandardAPI('updateServer', params, { text: 'Updating server...' });
299
+ if (this.dry) return;
300
+ if (this.format.match(/json/)) return this.jsonOutput(data);
301
+ this.toast('✅', 'green', 'Successfully updated server: #' + server.id);
302
+ },
303
+
304
+ async createServerSnapshotFromCLI(server, online) {
305
+ if (this.args.snapshot !== true) this.die('Server --snapshot must be specified without a value.');
306
+ delete this.args.snapshot;
307
+ if (Tools.numKeys(this.args)) this.die('Unsupported Server snapshot option: --' + Tools.firstKey(this.args));
308
+ if (!online) this.die('Cannot create a snapshot for an offline server.');
309
+
310
+ this.printMutationSummary({
311
+ title: 'Create Server Snapshot',
312
+ rows: this.getServerCommandSummaryRows(server, online)
313
+ });
314
+ var data = await this.callStandardAPI('createSnapshot', { server: server.id }, { text: 'Creating server snapshot...' });
315
+ if (this.dry) return;
316
+ if (this.format.match(/json/)) return this.jsonOutput(data);
317
+ this.toast('✅', 'green', 'Successfully created server snapshot: #' + data.id);
318
+ this.printSuggestedCommands({
319
+ 'View server details': 'xy server ' + server.id,
320
+ 'Set a five-minute watch': 'xy server ' + server.id + ' --watch 300'
321
+ });
322
+ },
323
+
324
+ async watchServerFromCLI(server, online) {
325
+ var duration = this.args.watch;
326
+ delete this.args.watch;
327
+ if (Tools.numKeys(this.args)) this.die('Unsupported Server watch option: --' + Tools.firstKey(this.args));
328
+ if (!Number.isInteger(duration) || (duration < 0)) this.die('Server --watch must be a non-negative integer number of seconds.');
329
+ if (!online) this.die('Cannot set or remove a watch on an offline server.');
330
+
331
+ this.printMutationSummary({
332
+ title: duration ? 'Set Server Watch' : 'Remove Server Watch',
333
+ rows: this.getServerCommandSummaryRows(server, online).concat([
334
+ ['Duration', duration ? Tools.getTextFromSeconds(duration, false, true) : gray('Disabled')]
335
+ ])
336
+ });
337
+ var data = await this.callStandardAPI('watchServer', {
338
+ id: server.id,
339
+ duration: duration
340
+ }, { text: duration ? 'Setting server watch...' : 'Removing server watch...' });
341
+ if (this.dry) return;
342
+ if (this.format.match(/json/)) return this.jsonOutput(data);
343
+ this.toast('✅', 'green', duration ? 'Successfully set server watch for ' + Tools.getTextFromSeconds(duration, false, true) + '.' : 'Successfully removed server watch.');
344
+ },
345
+
346
+ async deleteServerFromCLI(server, online) {
347
+ if (this.args.delete !== true) this.die('Server --delete must be specified without a value.');
348
+ if (('confirm' in this.args) && (typeof(this.args.confirm) != 'boolean')) this.die('Server delete --confirm must be true or false.');
349
+ var confirmed = this.args.confirm === true;
350
+ delete this.args.delete;
351
+ delete this.args.confirm;
352
+ if (Tools.numKeys(this.args)) this.die('Unsupported Server delete option: --' + Tools.firstKey(this.args));
353
+
354
+ this.printMutationSummary({
355
+ title: 'Delete Server',
356
+ rows: this.getServerCommandSummaryRows(server, online).concat([
357
+ online ? ['xySat', red('Uninstall')] : null,
358
+ ['Server Record', red('Delete')],
359
+ ['Monitoring History', red('Delete')],
360
+ ['Snapshots', red('Delete')]
361
+ ])
362
+ });
363
+ if (!confirmed) {
364
+ this.toast('⚠️', 'orange', "This will permanently delete the server record, all monitoring history, and all snapshots" + (online ? ', and uninstall xySat' : '') + ". Review the server above, then add '--confirm' to continue.");
365
+ return;
366
+ }
367
+
368
+ var data = await this.callStandardAPI('deleteServer', {
369
+ id: server.id,
370
+ history: true
371
+ }, { text: 'Deleting server and monitoring history...' });
372
+ if (this.dry) return;
373
+ if (this.format.match(/json/)) return this.jsonOutput(data);
374
+ this.toast('✅', 'green', 'Server deletion has started in the background: #' + server.id);
375
+ },
376
+
377
+ async cmd_server_history() {
378
+ // Historical views always cover exactly one hour, day, month or year. The
379
+ // global page and limit options apply only to the alert and job tables.
380
+ this.prepSearchArgs();
381
+ var selector = this.args.other.shift();
382
+ var date_spec = this.args.other.shift();
383
+ if (!selector || !date_spec || this.args.other.length) return this.dieUsage('server history');
384
+ delete this.args.other;
385
+ if (Tools.numKeys(this.args)) {
386
+ this.die('Unsupported Server history option: "--' + Tools.firstKey(this.args) + '".');
387
+ }
388
+
389
+ var range = this.parseServerHistoryRange(date_spec);
390
+ await this.getMultiple();
391
+ var local_server = this.findLocalServer(selector);
392
+ var id = local_server ? local_server.id : selector;
393
+ var alert_query = 'server:' + id + ' start:<' + range.end + ' end:>=' + range.start;
394
+ var job_query = 'server:' + id + ' date:>=' + range.start + ' date:<' + range.end;
395
+
396
+ cli.progress.start({ amount: 1, pct: false, text: gray('\u2192 Loading historical server data...') });
397
+ var [server_result, monitor_result, alert_result, job_result] = await Promise.all([
398
+ this.api.getServer({ id: id }),
399
+ this.api.getHistoricalMonitorData({
400
+ server: id,
401
+ sys: range.mode,
402
+ date: range.monitor_start,
403
+ limit: range.monitor_limit
404
+ }),
405
+ this.api.searchAlerts({
406
+ query: alert_query,
407
+ offset: this.offset,
408
+ limit: this.limit,
409
+ sort_by: '_id',
410
+ sort_dir: -1,
411
+ ttl: 1
412
+ }),
413
+ this.api.searchJobs({
414
+ query: job_query,
415
+ offset: this.offset,
416
+ limit: this.limit
417
+ })
418
+ ]);
419
+ cli.progress.end();
420
+
421
+ var failed = [server_result, monitor_result, alert_result, job_result].find( result => result.err );
422
+ if (failed) this.die(failed.err);
423
+
424
+ var server_response = server_result.data;
425
+ var server = server_response.server;
426
+ var snapshot = server_response.data || {};
427
+ var monitor_rows = monitor_result.data.rows || [];
428
+ var alert_rows = alert_result.data.rows || [];
429
+ var job_rows = job_result.data.rows || [];
430
+ var alerts = {
431
+ rows: alert_rows,
432
+ list: alert_result.data.list || { length: alert_rows.length }
433
+ };
434
+ var jobs = {
435
+ rows: job_rows,
436
+ list: job_result.data.list || { length: job_rows.length }
437
+ };
438
+
439
+ var output = {
440
+ server: server,
441
+ data: snapshot,
442
+ online: !!server_response.online,
443
+ range: range,
444
+ monitors: monitor_rows,
445
+ alerts: alerts,
446
+ jobs: jobs
447
+ };
448
+ if (this.format.match(/json/)) return this.jsonOutput(output);
449
+
450
+ println('\n ' + green.bold(range.mode.toUpperCase() + ' SERVER HISTORY - ' + range.title));
451
+ this.printServerHistoricalSummary(server, snapshot);
452
+ this.printServerHistoricalMonitorCharts(server, monitor_rows, range);
453
+ this.printServerHistoricalAlerts(alerts, range);
454
+ this.printServerHistoricalJobs(jobs, range);
455
+
456
+ var prefix = 'xy server history ' + server.id + ' ';
457
+ this.printSuggestedCommands({
458
+ 'View live or last-known state': 'xy server ' + server.id,
459
+ 'View hourly history': range.mode == 'hourly' ? '' : prefix + range.examples.hourly,
460
+ 'View daily history': range.mode == 'daily' ? '' : prefix + range.examples.daily,
461
+ 'View monthly history': range.mode == 'monthly' ? '' : prefix + range.examples.monthly,
462
+ 'View yearly history': range.mode == 'yearly' ? '' : prefix + range.examples.yearly
463
+ });
464
+ },
465
+
466
+ async cmd_get_servers() {
467
+ // getMultiple contains both currently connected servers and the recent
468
+ // offline cache. Merge copies locally so neither shared collection is
469
+ // modified while decorating cached records for display.
470
+ this.prepSearchArgs();
471
+ await this.getMultiple();
472
+
473
+ var servers = [];
474
+ var active_ids = {};
475
+ Object.values(this.servers || {}).filter( item => !!item ).forEach( item => {
476
+ active_ids[item.id] = true;
477
+ servers.push( Object.assign({}, item) );
478
+ });
479
+ Object.values(this.serverCache || {}).filter( item => !!item ).forEach( item => {
480
+ if (!active_ids[item.id]) servers.push( Object.assign({}, item, { offline: true }) );
481
+ });
482
+
483
+ var is_filtered = !!this.args.other.length;
484
+ if (is_filtered) {
485
+ var search = this.args.other.join(' ').trim();
486
+ servers = servers.filter( server => this.getServerSearchText(server).includes(search.toLowerCase()) );
487
+ }
488
+ delete this.args.other;
489
+
490
+ Object.keys(this.args).forEach( key => {
491
+ if (!SERVER_LIST_FILTERS.includes(key)) {
492
+ var suggestion = this.findClosestString(key, SERVER_LIST_FILTERS);
493
+ this.die('Unsupported Server list option: "--' + key + '".' + (suggestion ? ' Did you mean "--' + suggestion + '"?' : ''));
494
+ }
495
+
496
+ servers = this.filterActiveServers(servers, key, this.args[key]);
497
+ is_filtered = true;
498
+ });
499
+
500
+ servers.sort( function(a, b) {
501
+ var a_label = String(a.title || a.hostname || a.id).toLowerCase();
502
+ var b_label = String(b.title || b.hostname || b.id).toLowerCase();
503
+ return a_label.localeCompare(b_label) || String(a.id).localeCompare(String(b.id));
504
+ });
505
+
506
+ if (this.format.match(/json/)) return this.jsonOutput(servers);
507
+
508
+ this.printPaginatedBoxTable({
509
+ title: is_filtered ? 'Filtered Servers' : 'Active Servers',
510
+ header: ['Server ID', 'Server', 'Status', 'IP Address', 'OS', 'Arch', 'Groups', 'CPUs', 'RAM', 'Jobs', 'Alerts'],
511
+ rows: servers.slice(this.offset, this.offset + this.limit),
512
+ list: { length: servers.length },
513
+ offset: this.offset,
514
+ limit: this.limit
515
+ }, server => this.getServerTableRow(server, true));
516
+
517
+ this.printSuggestedCommands({
518
+ 'Add a Linux server': 'xy server add --platform linux',
519
+ 'Search server history': 'xy server search SEARCH_TEXT',
520
+ 'Filter by operating system': 'xy servers --os linux'
521
+ });
522
+ },
523
+
524
+ filterActiveServers(servers, key, value) {
525
+ // Named filters are ANDed together by the caller. Text fields use friendly
526
+ // case-insensitive substring matching because this view is entirely local.
527
+ if ((key == 'online') || (key == 'enabled')) {
528
+ var expected = this.parseServerBoolean(value, key);
529
+ return servers.filter( server => {
530
+ return key == 'online' ? !server.offline === expected : !!server.enabled === expected;
531
+ });
532
+ }
533
+
534
+ var aliases = {
535
+ platform: 'os_platform',
536
+ distro: 'os_distro',
537
+ release: 'os_release',
538
+ arch: 'os_arch',
539
+ group: 'groups'
540
+ };
541
+ var field = aliases[key] || key;
542
+ var values = this.parseServerList(value, key);
543
+ if (!values.length) this.die('Server list option cannot be empty: --' + key);
544
+
545
+ return servers.filter( server => {
546
+ var info = this.getServerRecordInfo(server);
547
+ var fields = {
548
+ id: server.id,
549
+ title: server.title,
550
+ hostname: server.hostname,
551
+ ip: info.ip,
552
+ os: [info.platform, info.distro, info.release].join(' '),
553
+ os_platform: info.platform,
554
+ os_distro: info.distro,
555
+ os_release: info.release,
556
+ os_arch: info.arch,
557
+ cpu: [info.cpu.brand, info.cpu.combo].join(' '),
558
+ satellite: info.satellite,
559
+ groups: this.getServerGroupSearchText(server),
560
+ status: server.offline ? 'offline' : (server.enabled ? 'online' : 'disabled')
561
+ };
562
+ var haystack = String(fields[field] || '').toLowerCase();
563
+ return values.every( item => haystack.includes(String(item).toLowerCase()) );
564
+ });
565
+ },
566
+
567
+ async cmd_add_server() {
568
+ // Generate a short-lived bootstrap token, then expand the UI's canonical
569
+ // one-line installer template. The CLI never executes the resulting code.
570
+ if (this.args.other.length) return this.dieUsage('server add');
571
+ var platform = String(this.args.platform || '').toLowerCase();
572
+ if (!platform) return this.dieUsage('server add');
573
+ if (!SERVER_PLATFORMS[platform]) this.die('Server platform must be one of: ' + Object.keys(SERVER_PLATFORMS).join(', ') + '.');
574
+
575
+ var params = {};
576
+ if ('title' in this.args) {
577
+ if (typeof(this.args.title) != 'string') this.die('Server title must be text.');
578
+ params.title = this.args.title.trim();
579
+ }
580
+ if ('enabled' in this.args) params.enabled = this.parseServerBoolean(this.args.enabled, 'enabled');
581
+ if ('icon' in this.args) {
582
+ if ((typeof(this.args.icon) != 'string') || !this.args.icon.trim()) this.die('Server icon must be text.');
583
+ params.icon = this.args.icon.trim().replace(/^mdi\-/, '');
584
+ }
585
+ if ('groups' in this.args) {
586
+ params.groups = this.parseServerList(this.args.groups, 'groups');
587
+ await this.getMultiple();
588
+ params.groups.forEach( id => {
589
+ if (Tools.findObject(this.groups, { id: id })) return;
590
+ var suggestion = this.findClosestString(id, this.groups.map( group => group.id ));
591
+ this.die('Unknown server Group ID: "' + id + '".' + (suggestion ? ' Did you mean "' + suggestion + '"?' : ''));
592
+ });
593
+ }
594
+ if ('expires' in this.args) {
595
+ if (!Number.isInteger(this.args.expires) || (this.args.expires < 1)) this.die('Server token expiration must be a positive integer number of seconds.');
596
+ params.expires = this.args.expires;
597
+ }
598
+
599
+ delete this.args.other;
600
+ delete this.args.platform;
601
+ delete this.args.title;
602
+ delete this.args.enabled;
603
+ delete this.args.icon;
604
+ delete this.args.groups;
605
+ delete this.args.expires;
606
+ if (Tools.numKeys(this.args)) return this.die('Unsupported Server add option: --' + Tools.firstKey(this.args));
607
+
608
+ var data = await this.callStandardAPI('getSatelliteToken', params, { text: 'Generating server install command...' });
609
+ if (this.dry) return;
610
+
611
+ var template_key = SERVER_PLATFORMS[platform];
612
+ var templates = (this.config.ui && this.config.ui.satellite_install_commands) || {};
613
+ var template = templates[template_key];
614
+ if (!template) this.die('No server install command is configured for platform: ' + platform);
615
+ var install = Tools.sub(template, Object.assign({}, data, {
616
+ unique: 'd' + Date.now().toString(36)
617
+ }));
618
+ var result = { platform: platform, command: install };
619
+ if (this.format.match(/json/)) return this.jsonOutput(result);
620
+
621
+ this.printBoxList({
622
+ title: 'Add Server',
623
+ rows: [
624
+ ['Platform', platform],
625
+ params.title ? ['Title', params.title] : null,
626
+ ('enabled' in params) ? ['Enabled', params.enabled ? green('Yes') : gray('No')] : null,
627
+ params.icon ? ['Icon', params.icon] : null,
628
+ params.groups ? ['Groups', params.groups.length ? params.groups.join(', ') : gray('(Automatic)')] : null,
629
+ ['Token Valid For', Tools.getTextFromSeconds(params.expires || 86400, false, true)]
630
+ ]
631
+ });
632
+ println('\n ' + this.color('theme').bold('SERVER INSTALL COMMAND') + '\n\n' + install);
633
+
634
+ this.printSuggestedCommands({
635
+ 'List active servers': 'xy servers',
636
+ 'Generate a Windows installer': 'xy server add --platform windows',
637
+ 'Generate a Docker installer': 'xy server add --platform docker',
638
+ 'Search server history': 'xy server search'
639
+ });
640
+ },
641
+
642
+ async cmd_search_servers() {
643
+ // Database search uses Unbase query syntax. Positional text is preserved as
644
+ // a raw query, while named options provide a safer shorthand for indexed
645
+ // fields documented by xyOps.
646
+ this.prepSearchArgs();
647
+ await this.getMultiple();
648
+
649
+ var query_parts = [];
650
+ if (this.args.other.length) query_parts.push(this.args.other.join(' ').trim());
651
+ delete this.args.other;
652
+ if ('query' in this.args) {
653
+ if ((typeof(this.args.query) != 'string') || !this.args.query.trim()) this.die('Server search query must be a non-empty string.');
654
+ query_parts.push(this.args.query.trim());
655
+ delete this.args.query;
656
+ }
657
+
658
+ var aliases = {
659
+ group: 'groups',
660
+ os: 'os_platform',
661
+ platform: 'os_platform',
662
+ distro: 'os_distro',
663
+ release: 'os_release',
664
+ arch: 'os_arch',
665
+ virt: 'cpu_virt',
666
+ cpu: 'cpu_brand'
667
+ };
668
+ Object.keys(this.args).forEach( key => {
669
+ var field = aliases[key] || key;
670
+ if (!SERVER_SEARCH_FIELDS.includes(field)) {
671
+ var choices = SERVER_SEARCH_FIELDS.concat(Object.keys(aliases), ['query']);
672
+ var suggestion = this.findClosestString(key, choices);
673
+ this.die('Unsupported Server search option: "--' + key + '".' + (suggestion ? ' Did you mean "--' + suggestion + '"?' : ''));
674
+ }
675
+
676
+ var values = this.parseServerList(this.args[key], key);
677
+ if (!values.length) this.die('Server search option cannot be empty: --' + key);
678
+ if (field == 'groups') values = values.map( value => this.resolveServerGroup(value) );
679
+ if ((field == 'created') || (field == 'modified')) {
680
+ values.forEach( value => query_parts.push(this.getDateRangeQuery(field, value) || (field + ':' + value)) );
681
+ }
682
+ else {
683
+ query_parts.push(field + ':' + values.map( this.quoteServerQueryValue ).join('|'));
684
+ }
685
+ });
686
+
687
+ var query = query_parts.filter( part => !!part ).join(' ').trim() || '*';
688
+ cli.progress.start({ amount: 1, pct: false, text: gray('→ Searching Servers...') });
689
+ var { err, data } = await this.api.searchServers({
690
+ query: query,
691
+ offset: this.offset,
692
+ limit: this.limit
693
+ });
694
+ cli.progress.end();
695
+ if (err) this.die(err);
696
+
697
+ var rows = (data.rows || []).map( server => {
698
+ return this.servers[server.id] ? Object.assign({}, server) : Object.assign({}, server, { offline: true });
699
+ });
700
+ if (this.format.match(/json/)) return this.jsonOutput(rows);
701
+
702
+ this.printPaginatedBoxTable({
703
+ title: query == '*' ? 'All Historical Servers' : 'Server Search Results',
704
+ header: ['Server ID', 'Server', 'Status', 'IP Address', 'OS', 'Arch', 'Groups', 'Created', 'Modified'],
705
+ rows: rows,
706
+ list: data.list || { length: rows.length },
707
+ offset: this.offset,
708
+ limit: this.limit
709
+ }, server => this.getServerTableRow(server, false));
710
+
711
+ this.printSuggestedCommands({
712
+ 'Search by operating system': 'xy server search --os_platform linux',
713
+ 'Search by architecture': 'xy server search --os_arch arm64',
714
+ 'List active servers': 'xy servers'
715
+ });
716
+ },
717
+
718
+ printServerHistoricalSummary(server, snapshot) {
719
+ // Historical pages use the compact three-row summary from the web UI.
720
+ // Hardware values come from the latest stored Server monitor record.
721
+ var info = this.getServerRecordInfo(server);
722
+ var data = snapshot.data || {};
723
+ var memory = data.memory || data.mem || info.memory || {};
724
+ var cpu = data.cpu || info.cpu || {};
725
+ var os_data = data.os || {};
726
+ var os = [os_data.distro || os_data.platform || data.platform || info.distro || info.platform, os_data.release || data.release || info.release].filter( item => !!item ).join(' ') || gray('(Unknown)');
727
+ var groups = (server.groups || []).map( id => this.getNiceGroup(id) ).join(', ') || gray('(None)');
728
+ var cpu_type = cpu.combo || cpu.brand || cpu.vendor || gray('(Unknown)');
729
+ var virt = info.virt.vendor || info.virt.type || gray('(None)');
730
+
731
+ this.printBoxList({
732
+ title: 'Server Summary',
733
+ rows: [
734
+ ['Server ID', this.color('theme').bold(server.id)],
735
+ ['Server Hostname', server.hostname || gray('(Unknown)')],
736
+ ['Server IP', info.ip || snapshot.ip || gray('(Unknown)')],
737
+ ['Server Label', server.title || 'n/a'],
738
+ ['Groups', groups],
739
+ ['Architecture', data.arch || os_data.arch || info.arch || gray('(Unknown)')],
740
+ ['Operating System', os],
741
+ ['Server Uptime', Tools.getTextFromSeconds(data.uptime_sec || 0, false, true)],
742
+ ['Total RAM', this.formatServerBytes(memory.total || 0)],
743
+ ['CPU Cores', Tools.commify(cpu.physicalCores || 0) + ' physical, ' + Tools.commify(cpu.cores || 0) + ' virtual'],
744
+ ['CPU Type', cpu_type],
745
+ ['Virtualization', virt]
746
+ ]
747
+ });
748
+ },
749
+
750
+ printServerHistoricalMonitorCharts(server, rows, range) {
751
+ // Unlike the live one-hour monitor view, the historical API has already
752
+ // selected the exact requested range, so no additional pruning is needed.
753
+ var monitors = this.getVisibleServerMonitors(server);
754
+ println('\n ' + this.color('theme').bold('SERVER MONITORS - ' + range.title.toUpperCase()));
755
+ if (!monitors.length) {
756
+ println(' ' + gray('No visible monitors apply to this server.'));
757
+ return;
758
+ }
759
+ if (!rows.length) {
760
+ println(' ' + gray('No monitoring data found in the selected range.'));
761
+ return;
762
+ }
763
+
764
+ monitors.forEach( (def, idx) => {
765
+ var data = this.getServerMonitorChartData(rows, def);
766
+ println('\n' + cli.chart({
767
+ title: def.title,
768
+ data: data,
769
+ dataType: def.data_type,
770
+ dataSuffix: def.suffix || '',
771
+ minVertScale: def.min_vert_scale || 0,
772
+ delta: !!def.delta,
773
+ deltaMinValue: def.delta_min_value ?? false,
774
+ divideByDelta: !!def.divide_by_delta,
775
+ height: 10,
776
+ indent: 1,
777
+ color: this.getServerChartColor(idx)
778
+ }));
779
+ });
780
+ },
781
+
782
+ printServerHistoricalAlerts(result, range) {
783
+ if (!result.rows.length) {
784
+ println('\n ' + this.color('theme').bold(('Server Alerts - ' + range.title).toUpperCase()));
785
+ println(' ' + gray('No server alerts found in the selected range.'));
786
+ return;
787
+ }
788
+
789
+ this.printPaginatedBoxTable({
790
+ title: 'Server Alerts - ' + range.title,
791
+ header: ['Alert ID', 'Title', 'Message', 'Server', 'Status', 'Started', 'Duration'],
792
+ rows: result.rows,
793
+ list: result.list,
794
+ offset: this.offset,
795
+ limit: this.limit
796
+ }, alert => {
797
+ var completed = alert.active ? this.epoch : (alert.modified || alert.date);
798
+ var message = String(alert.message || '').replace(/\s+/g, ' ').trim();
799
+ return [
800
+ this.color('theme').bold(alert.id),
801
+ this.getNiceAlert(alert.alert),
802
+ message,
803
+ this.getNiceServer(alert.server),
804
+ this.getNiceAlertStatus(alert),
805
+ this.getRelativeDateTime(alert.date, true),
806
+ Tools.getTextFromSeconds(Math.max(0, completed - alert.date), true, true)
807
+ ];
808
+ });
809
+ },
810
+
811
+ printServerHistoricalJobs(result, range) {
812
+ if (!result.rows.length) {
813
+ println('\n ' + this.color('theme').bold(('Server Jobs - ' + range.title).toUpperCase()));
814
+ println(' ' + gray('No completed server jobs found in the selected range.'));
815
+ return;
816
+ }
817
+
818
+ this.printPaginatedBoxTable({
819
+ title: 'Server Jobs - ' + range.title,
820
+ header: ['Job ID', 'Server', 'Source', 'Started', 'Elapsed', 'Avg CPU/Mem', 'Result'],
821
+ rows: result.rows,
822
+ list: result.list,
823
+ offset: this.offset,
824
+ limit: this.limit
825
+ }, job => {
826
+ var avg_cpu = (job.cpu && job.cpu.count) ? (job.cpu.total / job.cpu.count) : 0;
827
+ var avg_mem = (job.mem && job.mem.count) ? (job.mem.total / job.mem.count) : 0;
828
+ return [
829
+ this.color('theme').bold(this.getNiceJob(job)),
830
+ this.getNiceServer(job.server),
831
+ this.getNiceJobSource(job),
832
+ this.getRelativeDateTime(job.started, true),
833
+ this.getNiceJobElapsedTime(job, true, true),
834
+ this.formatServerPercent(avg_cpu) + ' / ' + this.formatServerBytes(avg_mem),
835
+ this.getNiceJobResult(job)
836
+ ];
837
+ });
838
+ },
839
+
840
+ printServerSummary(server, snapshot, online) {
841
+ // Match the four rows of the web UI summary while adapting them to a
842
+ // compact terminal definition list.
843
+ var info = this.getServerRecordInfo(server);
844
+ var data = snapshot.data || {};
845
+ var day = (((this.stats || {}).currentDay || {}).servers || {})[server.id] || {};
846
+ var completed = day.job_complete || 0;
847
+ var success = day.job_success || 0;
848
+ var groups = (server.groups || []).map( id => this.getNiceGroup(id) ).join(', ') || gray('(None)');
849
+ var cpu_type = info.cpu.combo || info.cpu.brand || info.cpu.vendor || gray('(Unknown)');
850
+ var virt = info.virt.vendor || info.virt.type || gray('(None)');
851
+ var os = [info.distro || info.platform, info.release].filter( item => !!item ).join(' ') || gray('(Unknown)');
852
+ var status = online ? green('Online (Live)') : gray('Offline (Last Known State)');
853
+
854
+ this.printBoxList({
855
+ title: 'Server Summary',
856
+ rows: [
857
+ ['Status', status],
858
+ !online && snapshot.date ? ['Last Updated', this.getRelativeDateTime(snapshot.date, true)] : null,
859
+ ['Server ID', this.color('theme').bold(server.id)],
860
+ server.title ? ['Title', bold(server.title)] : null,
861
+ ['Server Hostname', server.hostname || gray('(Unknown)')],
862
+ ['Server IP', info.ip || gray('(Unknown)')],
863
+ ['xySat Version', info.satellite ? ('v' + info.satellite.replace(/^v/, '')) : gray('(Unknown)')],
864
+ ['Groups (' + (server.autoGroup ? 'Auto' : 'Manual') + ')', groups],
865
+ ['Architecture', info.arch || gray('(Unknown)')],
866
+ ['Operating System', os],
867
+ ['Uptime', Tools.getTextFromSeconds(data.uptime_sec || 0, false, true)],
868
+ ['Total RAM', this.formatServerBytes(info.memory.total || 0)],
869
+ ['CPU Cores', Tools.commify(info.cpu.physicalCores || 0) + ' physical, ' + Tools.commify(info.cpu.cores || 0) + ' virtual'],
870
+ ['CPU Type', cpu_type],
871
+ ['Virtualization', virt],
872
+ ['Alerts Today', Tools.commify(day.alert_new || 0)],
873
+ ['Jobs Today', Tools.commify(completed)],
874
+ ['Jobs Failed Today', Tools.commify(day.job_error || 0)],
875
+ ['Job Success Rate', Tools.pct(success / (completed || 1))]
876
+ ]
877
+ });
878
+ },
879
+
880
+ printServerAlerts(server, snapshot, online) {
881
+ // Offline snapshots can preserve the alerts that were active when the
882
+ // Server disappeared. Live views use the current getMultiple collection.
883
+ var alerts = online ? Object.values(this.activeAlerts || {}).filter( alert => alert.server == server.id ) : (snapshot.alerts || []).filter( alert => alert.server == server.id );
884
+ alerts.sort( (a, b) => (b.date || 0) - (a.date || 0) );
885
+
886
+ this.printPaginatedBoxTable({
887
+ title: 'Server Alerts',
888
+ header: ['Alert ID', 'Title', 'Message', 'Status', 'Started', 'Duration'],
889
+ rows: alerts.slice(this.offset, this.offset + this.limit),
890
+ list: { length: alerts.length },
891
+ offset: this.offset,
892
+ limit: this.limit
893
+ }, alert => [
894
+ this.color('theme').bold(alert.id || '(Unknown)'),
895
+ this.getNiceAlert(alert.alert, false),
896
+ alert.message || '',
897
+ this.getNiceAlertStatus(alert),
898
+ alert.date ? this.getNiceDateTime(alert.date) : gray('(Unknown)'),
899
+ Tools.getTextFromSeconds(Math.max(0, (alert.end || this.epoch) - (alert.date || this.epoch)), true, true)
900
+ ]);
901
+ },
902
+
903
+ printServerJobs(server) {
904
+ // Active jobs are already decorated by getMultiple, so reuse the standard
905
+ // job renderer for consistent event, category and progress formatting.
906
+ var jobs = Object.values(this.activeJobs || {}).filter( job => job.server == server.id );
907
+ jobs.sort( (a, b) => (b.started || 0) - (a.started || 0) );
908
+ this.printActiveJobs({ title: 'Server Jobs', rows: jobs.slice(this.offset, this.offset + this.limit) });
909
+ },
910
+
911
+ printServerQuickmonCharts(server, rows, heading) {
912
+ var definitions = (this.config.quick_monitors || []).filter( def => {
913
+ return !def.groups || !def.groups.length || this.includesAny(server.groups || [], def.groups);
914
+ });
915
+ if (!definitions.length) return;
916
+
917
+ println('\n ' + this.color('theme').bold(heading || 'QUICK LOOK - LAST MINUTE'));
918
+ definitions.forEach( (def, idx) => {
919
+ var data = rows.map( row => {
920
+ // Current xyOps rows are keyed by monitor ID. The source fallback also
921
+ // supports unflattened samples and custom monitor implementations.
922
+ var value = Tools.getPath(row, def.source);
923
+ if (typeof(value) != 'number') value = row[def.id];
924
+ return { x: row.date, y: (typeof(value) == 'number') ? value : 0 };
925
+ });
926
+ println('\n' + cli.chart({
927
+ title: def.title,
928
+ data: data,
929
+ dataType: def.type,
930
+ dataSuffix: def.suffix || '',
931
+ minVertScale: def.min_vert_scale || 0,
932
+ delta: !!def.delta,
933
+ deltaMinValue: def.delta_min_value ?? false,
934
+ divideByDelta: !!def.divide_by_delta,
935
+ height: 10,
936
+ indent: 1,
937
+ color: this.getServerChartColor(idx)
938
+ }));
939
+ });
940
+ },
941
+
942
+ printServerMemory(data) {
943
+ var memory = data.memory || data.mem || {};
944
+ var total = memory.total || 0;
945
+ var metrics = [
946
+ ['Memory Used', 'used'],
947
+ ['Memory Active', 'active'],
948
+ ['Memory Available', 'available'],
949
+ ['Memory Free', 'free'],
950
+ ['Memory Buffered', 'buffers'],
951
+ ['Memory Cached', 'cached']
952
+ ];
953
+
954
+ this.printBoxTable({
955
+ title: 'Memory Details',
956
+ header: ['Metric', 'Value', 'Visual'],
957
+ rows: total ? metrics.map( metric => {
958
+ var value = Math.max(0, memory[metric[1]] || 0);
959
+ return [metric[0], this.formatServerBytes(value), cli.progress.bar({ amount: value, max: total || 1, width: 30, pct: false })];
960
+ }) : []
961
+ });
962
+ },
963
+
964
+ printServerCPU(data) {
965
+ var cpu = data.cpu || {};
966
+ var cpus = cpu.cpus || [];
967
+
968
+ this.printBoxTable({
969
+ title: 'CPU Details',
970
+ header: ['CPU #', 'User %', 'System %', 'Nice %', 'I/O Wait %', 'Hard IRQ %', 'Soft IRQ %', 'Total %'],
971
+ rows: cpus.map( (item, idx) => {
972
+ var total = Math.max(0, Math.min(100, 100 - (item.idle || 0)));
973
+ return [
974
+ '#' + (idx + 1),
975
+ this.formatServerPercent(item.user),
976
+ this.formatServerPercent(item.system),
977
+ this.formatServerPercent(item.nice),
978
+ this.formatServerPercent(item.iowait),
979
+ this.formatServerPercent(item.irq),
980
+ this.formatServerPercent(item.softirq),
981
+ cli.progress.bar({ amount: total, max: 100, width: 20, pct: true })
982
+ ];
983
+ })
984
+ });
985
+ },
986
+
987
+ printServerMonitorGrid(server, data) {
988
+ var monitors = this.getVisibleServerMonitors(server);
989
+ if (!monitors.length) return;
990
+ var units = monitors.map( def => {
991
+ var value = def.delta ? ((data.deltas || {})[def.id] || 0) : ((data.monitors || {})[def.id] || 0);
992
+ return [this.getServerMonitorLabel(def.title), this.formatServerMetric(value, def.data_type, def.suffix)];
993
+ });
994
+
995
+ println('\n ' + this.color('theme').bold('SERVER MONITOR SUMMARY'));
996
+ println('' + cli.dashGrid(units, {
997
+ minCols: 3,
998
+ maxCols: 6,
999
+ gap: 1,
1000
+ indent: 1,
1001
+ valueStyles: ['bold', 'green']
1002
+ }));
1003
+ },
1004
+
1005
+ printServerMonitorCharts(server, rows, online, heading) {
1006
+ var monitors = this.getVisibleServerMonitors(server);
1007
+ if (!monitors.length) return;
1008
+ var min_epoch = rows.length ? rows[rows.length - 1].date - 3600 : 0;
1009
+ rows = rows.filter( row => !min_epoch || (row.date >= min_epoch) );
1010
+
1011
+ println('\n ' + this.color('theme').bold(heading || ('SERVER MONITORS - ' + (online ? 'LAST HOUR' : 'LAST KNOWN STATE'))));
1012
+ monitors.forEach( (def, idx) => {
1013
+ var data = this.getServerMonitorChartData(rows, def);
1014
+ println('\n' + cli.chart({
1015
+ title: def.title,
1016
+ data: data,
1017
+ dataType: def.data_type,
1018
+ dataSuffix: def.suffix || '',
1019
+ minVertScale: def.min_vert_scale || 0,
1020
+ delta: !!def.delta,
1021
+ deltaMinValue: def.delta_min_value ?? false,
1022
+ divideByDelta: !!def.divide_by_delta,
1023
+ height: 10,
1024
+ indent: 1,
1025
+ color: this.getServerChartColor(idx)
1026
+ }));
1027
+ });
1028
+ },
1029
+
1030
+ printServerContainers(data) {
1031
+ var containers = Tools.getPath(data, 'docker.containers') || [];
1032
+ if (!containers.length) return;
1033
+ containers.sort( (a, b) => (b.cpu || 0) - (a.cpu || 0) );
1034
+
1035
+ this.printPaginatedBoxTable({
1036
+ title: 'Server Containers',
1037
+ header: ['Container Name', 'ID', 'CPU', 'Mem Usage', 'Mem Total', 'Net In', 'Net Out', 'Disk Read', 'Disk Write'],
1038
+ rows: containers.slice(this.offset, this.offset + this.limit),
1039
+ list: { length: containers.length },
1040
+ offset: this.offset,
1041
+ limit: this.limit
1042
+ }, item => [
1043
+ bold(item.name || '(Unknown)'),
1044
+ item.id || '',
1045
+ this.formatServerPercent(item.cpu),
1046
+ this.formatServerBytes(item.mem_usage || 0),
1047
+ this.formatServerBytes(item.mem_total || 0),
1048
+ this.formatServerBytes(item.net_read || 0),
1049
+ this.formatServerBytes(item.net_write || 0),
1050
+ this.formatServerBytes(item.disk_read || 0),
1051
+ this.formatServerBytes(item.disk_write || 0)
1052
+ ]);
1053
+ },
1054
+
1055
+ printServerProcesses(data) {
1056
+ var processes = ((data.processes || {}).list || []).slice(0);
1057
+ processes.sort( (a, b) => (b.cpu || 0) - (a.cpu || 0) );
1058
+
1059
+ this.printPaginatedBoxTable({
1060
+ title: 'Server Processes',
1061
+ header: ['Command', 'User', 'PID', 'Parent', 'CPU', 'Memory', 'Age', 'State'],
1062
+ rows: processes.slice(this.offset, this.offset + this.limit),
1063
+ list: { length: processes.length },
1064
+ offset: this.offset,
1065
+ limit: this.limit
1066
+ }, process => [
1067
+ bold(this.getServerProcessName(process)),
1068
+ process.user || '-',
1069
+ process.pid,
1070
+ process.parentPid || 'n/a',
1071
+ this.formatServerPercent(process.cpu),
1072
+ this.formatServerBytes(process.memRss || 0),
1073
+ Tools.getTextFromSeconds(process.age || 0, true, true),
1074
+ this.getServerTitleCase(process.state || 'unknown')
1075
+ ]);
1076
+ },
1077
+
1078
+ printServerProcessDetail(server, data, pid, detail) {
1079
+ detail = detail || this.getServerProcessDetailData(data, pid);
1080
+ var process = detail.process;
1081
+ var command = (process.path ? process.path.replace(/[\\\/]$/, '') + '/' : '') + (process.command || '');
1082
+ if (process.params) command += ' ' + process.params;
1083
+
1084
+ this.printBoxList({
1085
+ title: 'Process Details',
1086
+ rows: [
1087
+ ['Server', this.getNiceServer(server)],
1088
+ ['Command', command],
1089
+ ['Process ID', process.pid],
1090
+ process.parentPid ? ['Parent PID', process.parentPid] : null,
1091
+ ['User', process.user || 'n/a'],
1092
+ ['Group', process.group || 'n/a'],
1093
+ ['CPU', this.formatServerPercent(process.cpu)],
1094
+ ['Memory', this.formatServerPercent(process.mem)],
1095
+ ['Virtual Memory', this.formatServerBytes(process.memVsz || 0)],
1096
+ ['Resident Memory', this.formatServerBytes(process.memRss || 0)],
1097
+ ['Age', Tools.getTextFromSeconds(process.age || 0, false, true)],
1098
+ ['State', this.getServerTitleCase(process.state || 'unknown')],
1099
+ ['TTY', process.tty || 'n/a'],
1100
+ ['Priority', (process.priority === undefined) ? 'n/a' : process.priority],
1101
+ ['Nice', (process.nice === undefined) ? 'n/a' : process.nice],
1102
+ ['Threads', (process.threads === undefined) ? 'n/a' : process.threads],
1103
+ process.job ? ['Job ID', this.getNiceJob(process.job)] : null,
1104
+ process.job ? ['Total I/O', this.formatServerBytes(process.disk || 0)] : null,
1105
+ process.job ? ['Net Connections', Tools.commify(process.conns || 0)] : null,
1106
+ process.job ? ['Net Throughput', this.formatServerBytes(process.net || 0) + '/sec'] : null
1107
+ ]
1108
+ });
1109
+
1110
+ var family = detail.family;
1111
+ this.printBoxTable({
1112
+ title: 'Process Family',
1113
+ header: ['Command', 'User', 'PID'],
1114
+ rows: family.map( item => [
1115
+ ' '.repeat((item.indent || 0) * 2) + ((item.indent || 0) ? '\u2514 ' : '') + (item.pid == pid ? bold(this.getServerProcessName(item)) : this.getServerProcessName(item)),
1116
+ item.user || '-',
1117
+ item.pid
1118
+ ])
1119
+ });
1120
+ },
1121
+
1122
+ printServerConnections(data) {
1123
+ var processes = ((data.processes || {}).list || []);
1124
+ var connections = (data.conns || []).slice(0);
1125
+ connections.sort( (a, b) => String(a.state || '').localeCompare(String(b.state || '')) );
1126
+
1127
+ this.printPaginatedBoxTable({
1128
+ title: 'Network Connections',
1129
+ header: ['State', 'Protocol', 'Local Address', 'Remote Address', 'Process', 'Bytes In', 'Bytes Out'],
1130
+ rows: connections.slice(this.offset, this.offset + this.limit),
1131
+ list: { length: connections.length },
1132
+ offset: this.offset,
1133
+ limit: this.limit
1134
+ }, connection => {
1135
+ var process = connection.pid ? Tools.findObject(processes, { pid: connection.pid }) : null;
1136
+ return [
1137
+ this.getServerTitleCase(connection.state || 'unknown'),
1138
+ String(connection.type || '').toUpperCase(),
1139
+ connection.local_addr || '',
1140
+ connection.remote_addr || '',
1141
+ process ? this.getServerProcessName(process) : (connection.pid || gray('(None)')),
1142
+ this.formatServerBytes(connection.bytes_in || 0),
1143
+ this.formatServerBytes(connection.bytes_out || 0)
1144
+ ];
1145
+ });
1146
+ },
1147
+
1148
+ printServerInterfaces(data) {
1149
+ var interfaces = Object.values(data.interfaces || {});
1150
+ interfaces.sort( (a, b) => String(a.iface || '').localeCompare(String(b.iface || '')) );
1151
+
1152
+ this.printPaginatedBoxTable({
1153
+ title: 'Network Interfaces',
1154
+ header: ['Name', 'IPv4', 'IPv6', 'State', 'Type', 'Speed', 'Bytes In', 'Bytes Out'],
1155
+ rows: interfaces.slice(this.offset, this.offset + this.limit),
1156
+ list: { length: interfaces.length },
1157
+ offset: this.offset,
1158
+ limit: this.limit
1159
+ }, item => [
1160
+ item.iface || '',
1161
+ item.ip4 || 'n/a',
1162
+ item.ip6 || 'n/a',
1163
+ this.getServerTitleCase(item.operstate || 'unknown'),
1164
+ this.getServerTitleCase(item.type || 'unknown'),
1165
+ this.formatServerInterfaceSpeed(item.speed),
1166
+ this.formatServerBytes(item.rx_sec || 0) + '/sec',
1167
+ this.formatServerBytes(item.tx_sec || 0) + '/sec'
1168
+ ]);
1169
+ },
1170
+
1171
+ printServerFilesystems(data) {
1172
+ var mounts = Object.values(data.mounts || {});
1173
+ mounts.sort( (a, b) => String(a.mount || '').localeCompare(String(b.mount || '')) );
1174
+
1175
+ this.printPaginatedBoxTable({
1176
+ title: 'Filesystems',
1177
+ header: ['Mount Point', 'Type', 'Device', 'Total Size', 'Used', 'Available', 'Use %'],
1178
+ rows: mounts.slice(this.offset, this.offset + this.limit),
1179
+ list: { length: mounts.length },
1180
+ offset: this.offset,
1181
+ limit: this.limit
1182
+ }, item => [
1183
+ item.mount || '',
1184
+ item.type || '',
1185
+ item.fs || '',
1186
+ this.formatServerBytes(item.size || 0),
1187
+ this.formatServerBytes(item.used || 0),
1188
+ this.formatServerBytes(item.available || item.avail || 0),
1189
+ cli.progress.bar({ amount: item.use || 0, max: 100, width: 20, pct: true })
1190
+ ]);
1191
+ },
1192
+
1193
+ printServerExpansionNotice(title, option) {
1194
+ println('\n ' + this.color('theme').bold(title.toUpperCase()));
1195
+ println(' ' + gray('(Shown in verbose mode, or add ') + yellow.bold(option) + gray('.)'));
1196
+ },
1197
+
1198
+ findLocalServer(selector) {
1199
+ // Hostname and label resolution are intentionally limited to the two
1200
+ // in-memory lists returned by getMultiple. Historical searches remain explicit.
1201
+ if (this.servers[selector]) return this.servers[selector];
1202
+ if (this.serverCache[selector]) return this.serverCache[selector];
1203
+ var name = String(selector).toLowerCase();
1204
+ var matches = server => {
1205
+ return (String(server.hostname || '').toLowerCase() == name) || (String(server.title || '').toLowerCase() == name);
1206
+ };
1207
+ return Object.values(this.servers || {}).find(matches) || Object.values(this.serverCache || {}).find(matches);
1208
+ },
1209
+
1210
+ parseServerHistoryRange(value, label) {
1211
+ // Parse the compact YYYY[/MM[/DD[/HH]]] syntax in local Server time.
1212
+ // JavaScript's calendar constructor handles variable month lengths and DST.
1213
+ label = label || 'Server';
1214
+ value = String(value || '');
1215
+ if (!value.match(/^\d{4}(?:\/\d{1,2}){0,3}$/)) {
1216
+ this.die(label + ' history date must use YYYY, YYYY/MM, YYYY/MM/DD, or YYYY/MM/DD/HH format.');
1217
+ }
1218
+ var parts = value.split('/').map( part => parseInt(part) );
1219
+ var system = Object.keys(SERVER_HISTORY_SYSTEMS).map( key => Object.assign({ mode: key }, SERVER_HISTORY_SYSTEMS[key]) ).find( item => item.parts == parts.length );
1220
+ var year = parts[0];
1221
+ var month = parts[1] || 1;
1222
+ var day = parts[2] || 1;
1223
+ var hour = (parts[3] === undefined) ? 0 : parts[3];
1224
+ if ((year < 1970) || (year > 9999) || (month < 1) || (month > 12) || (day < 1) || (day > 31) || (hour < 0) || (hour > 23)) {
1225
+ this.die('Invalid ' + label + ' history date: ' + value);
1226
+ }
1227
+
1228
+ var start_date = new Date(year, month - 1, day, hour, 0, 0, 0);
1229
+ if ((start_date.getFullYear() != year) || (start_date.getMonth() != month - 1) || (start_date.getDate() != day) || (start_date.getHours() != hour)) {
1230
+ this.die('Invalid ' + label + ' history date: ' + value);
1231
+ }
1232
+
1233
+ var end_date = null;
1234
+ switch (system.mode) {
1235
+ case 'yearly': end_date = new Date(year + 1, 0, 1, 0, 0, 0, 0); break;
1236
+ case 'monthly': end_date = new Date(year, month, 1, 0, 0, 0, 0); break;
1237
+ case 'daily': end_date = new Date(year, month - 1, day + 1, 0, 0, 0, 0); break;
1238
+ case 'hourly': end_date = new Date(year, month - 1, day, hour + 1, 0, 0, 0); break;
1239
+ }
1240
+
1241
+ var start = Math.floor(start_date.getTime() / 1000);
1242
+ var end = Math.floor(end_date.getTime() / 1000);
1243
+ var monitor_start = Math.ceil(start / system.epoch_div) * system.epoch_div;
1244
+ var pad = number => String(number).padStart(2, '0');
1245
+ var timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
1246
+ return {
1247
+ mode: system.mode,
1248
+ input: value,
1249
+ title: this.formatServerHistoryTitle(start, system.mode, timezone),
1250
+ timezone: timezone,
1251
+ start: start,
1252
+ end: end,
1253
+ monitor_start: monitor_start,
1254
+ monitor_limit: Math.max(1, Math.ceil((end - monitor_start) / system.epoch_div)),
1255
+ examples: {
1256
+ hourly: year + '/' + pad(month) + '/' + pad(day) + '/' + pad(hour),
1257
+ daily: year + '/' + pad(month) + '/' + pad(day),
1258
+ monthly: year + '/' + pad(month),
1259
+ yearly: String(year)
1260
+ }
1261
+ };
1262
+ },
1263
+
1264
+ formatServerHistoryTitle(epoch, mode, timezone) {
1265
+ var options = { year: 'numeric', timeZone: timezone };
1266
+ if (mode != 'yearly') options.month = 'long';
1267
+ if ((mode == 'daily') || (mode == 'hourly')) options.day = 'numeric';
1268
+ if (mode == 'hourly') options.hour = 'numeric';
1269
+ options = this.getDateOptions(options);
1270
+ var locale = options.locale;
1271
+ delete options.locale;
1272
+ return new Intl.DateTimeFormat(locale, options).format( new Date(epoch * 1000) );
1273
+ },
1274
+
1275
+ getServerMonitorChartData(rows, def) {
1276
+ var data = rows.filter( row => row.date && row.totals ).map( row => {
1277
+ return { x: row.date, y: ((row.totals[def.id] || 0) / (row.count || 1)) };
1278
+ });
1279
+
1280
+ // A partial final delta bucket cannot be averaged reliably.
1281
+ if (def.delta && (rows.length > 1) && (rows[rows.length - 1].count < rows[rows.length - 2].count)) data.pop();
1282
+ return data;
1283
+ },
1284
+
1285
+ getVisibleServerMonitors(server) {
1286
+ return (this.monitors || []).filter( def => {
1287
+ return def.display && (!def.groups || !def.groups.length || this.includesAny(def.groups, server.groups || []));
1288
+ }).sort( (a, b) => (a.sort_order || 0) - (b.sort_order || 0) || String(a.title).localeCompare(String(b.title)) );
1289
+ },
1290
+
1291
+ getServerMonitorLabel(title) {
1292
+ return String(title || '')
1293
+ .replace(/\bTotal\b/g, '')
1294
+ .replace(/\bAverage\b/g, 'Avg')
1295
+ .replace(/\bOperations\b/g, 'Ops')
1296
+ .replace(/\bConnections\b/g, 'Conns')
1297
+ .replace(/\bAvailable\b/g, 'Avail')
1298
+ .replace(/\bMemory\b/g, 'Mem')
1299
+ .replace(/\(.+\)/g, '')
1300
+ .replace(/%/g, '')
1301
+ .replace(/\s+/g, ' ').trim();
1302
+ },
1303
+
1304
+ formatServerMetric(value, type, suffix) {
1305
+ value = Math.max(0, Number(value) || 0);
1306
+ var output = '';
1307
+ switch (type) {
1308
+ case 'integer': output = this.formatServerCompactNumber(value); break;
1309
+ case 'float': output = this.formatServerNumber(value); break;
1310
+ case 'bytes': output = this.formatServerCompactBytes(value); break;
1311
+ case 'seconds': output = Tools.getTextFromSeconds(value, false, true); break;
1312
+ case 'milliseconds': output = (value < 1000) ? (this.formatServerNumber(value) + ' ms') : Tools.getTextFromSeconds(value / 1000, false, true); break;
1313
+ default: output = this.formatServerNumber(value); break;
1314
+ }
1315
+ if (suffix) output += String(suffix).replace(/^\s*\/(\w)\w+$/, '/$1');
1316
+ return output;
1317
+ },
1318
+
1319
+ formatServerBytes(value) {
1320
+ return Tools.getTextFromBytes(Math.max(0, Number(value) || 0), 1).replace(/bytes/i, 'B');
1321
+ },
1322
+
1323
+ formatServerCompactBytes(value) {
1324
+ var units = ['B', 'K', 'M', 'G', 'T', 'P'];
1325
+ value = Math.max(0, Number(value) || 0);
1326
+ var idx = 0;
1327
+ while ((value >= 1024) && (idx < units.length - 1)) { value /= 1024; idx++; }
1328
+ return this.formatServerNumber(value) + units[idx];
1329
+ },
1330
+
1331
+ formatServerCompactNumber(value) {
1332
+ var units = ['', 'K', 'M', 'B', 'T'];
1333
+ value = Math.max(0, Number(value) || 0);
1334
+ var idx = 0;
1335
+ while ((value >= 1000) && (idx < units.length - 1)) { value /= 1000; idx++; }
1336
+ return this.formatServerNumber(value) + units[idx];
1337
+ },
1338
+
1339
+ formatServerNumber(value) {
1340
+ return String(Math.round((Number(value) || 0) * 100) / 100);
1341
+ },
1342
+
1343
+ formatServerPercent(value) {
1344
+ return this.formatServerNumber(value) + '%';
1345
+ },
1346
+
1347
+ formatServerInterfaceSpeed(speed) {
1348
+ if (!speed) return 'n/a';
1349
+ if (speed >= 1000000) return Math.floor(speed / 1000000) + ' Tb';
1350
+ if (speed >= 1000) return Math.floor(speed / 1000) + ' Gb';
1351
+ return speed + ' Mb';
1352
+ },
1353
+
1354
+ getServerChartColor(idx) {
1355
+ var colors = ['green', 'cyan', 'yellow', 'magenta', 'blue', 'red'];
1356
+ return colors[idx % colors.length];
1357
+ },
1358
+
1359
+ getServerProcessName(process) {
1360
+ if (process.name) {
1361
+ if ((process.name == 'svchost.exe') && process.command && process.command.match(/\s+\-s\s+(\S+)/)) return RegExp.$1;
1362
+ return process.name;
1363
+ }
1364
+ var command = String(process.command || '');
1365
+ command = command.replace(/\s[\-\(\/\*].*$/, '');
1366
+ if (command.match(/^\w:\\/)) command = command.replace(/\\$/, '').replace(/^(.*)\\([^\\]+)$/, '$2');
1367
+ else if (command.match(/^\//)) command = command.replace(/\/$/, '').replace(/^.*\//, '');
1368
+ if ((command.length > 32) && command.match(/\s/)) command = command.replace(/:?\s.*$/, '');
1369
+ return command.replace(/:\s+.*/, '') || '(Unknown)';
1370
+ },
1371
+
1372
+ getServerProcessFamily(processes, selected) {
1373
+ var rows = [Object.assign({}, selected)];
1374
+ var visited = { [selected.pid]: true };
1375
+ var add_children = (parent, indent) => {
1376
+ processes.filter( item => item.parentPid == parent.pid ).forEach( item => {
1377
+ if (visited[item.pid]) return;
1378
+ visited[item.pid] = true;
1379
+ rows.push( Object.assign({}, item, { indent: indent }) );
1380
+ add_children(item, indent + 1);
1381
+ });
1382
+ };
1383
+ add_children(selected, 1);
1384
+
1385
+ var process = selected;
1386
+ while (process.parentPid) {
1387
+ var parent = Tools.findObject(processes, { pid: process.parentPid });
1388
+ if (!parent || visited[parent.pid]) break;
1389
+ visited[parent.pid] = true;
1390
+ rows.forEach( item => { item.indent = (item.indent || 0) + 1; } );
1391
+ rows.unshift( Object.assign({}, parent) );
1392
+ process = parent;
1393
+ }
1394
+ return rows;
1395
+ },
1396
+
1397
+ getServerProcessDetailData(data, pid) {
1398
+ var processes = ((data.processes || {}).list || []);
1399
+ var process = Tools.findObject(processes, { pid: pid });
1400
+ if (!process) this.die('Could not find process ID ' + pid + ' in the latest server data.');
1401
+ return {
1402
+ process: process,
1403
+ family: this.getServerProcessFamily(processes, process)
1404
+ };
1405
+ },
1406
+
1407
+ getServerTitleCase(value) {
1408
+ return String(value).split(/[_\s]+/).filter( word => !!word ).map( word => word.substring(0, 1).toUpperCase() + word.substring(1).toLowerCase() ).join(' ');
1409
+ },
1410
+
1411
+ getServerTableRow(server, active_view) {
1412
+ var info = this.getServerRecordInfo(server);
1413
+ var num_jobs = Object.values(this.activeJobs || {}).filter( job => job.server == server.id ).length;
1414
+ var num_alerts = Object.values(this.activeAlerts || {}).filter( alert => alert.server == server.id ).length;
1415
+ var status = server.offline ? gray('Offline') : (server.enabled ? green('Online') : yellow('Disabled'));
1416
+ var os = [info.distro || info.platform, info.release].filter( item => !!item ).join(' ') || gray('(Unknown)');
1417
+ var groups = (server.groups || []).length ? server.groups.map( id => this.getNiceGroup(id) ).join(', ') : gray('(None)');
1418
+ var row = [
1419
+ this.color('theme').bold(server.id),
1420
+ bold(server.title || server.hostname || server.id),
1421
+ status,
1422
+ info.ip || gray('(Unknown)'),
1423
+ os,
1424
+ info.arch || gray('(Unknown)'),
1425
+ groups
1426
+ ];
1427
+
1428
+ if (active_view) {
1429
+ row.push(
1430
+ Tools.commify(info.cpu.cores || 0),
1431
+ Tools.getTextFromBytes(info.memory.total || 0, 1).replace(/bytes/, 'B'),
1432
+ Tools.commify(num_jobs),
1433
+ Tools.commify(num_alerts)
1434
+ );
1435
+ }
1436
+ else {
1437
+ row.push(
1438
+ this.getRelativeDateTime(server.created, true),
1439
+ this.getRelativeDateTime(server.modified, true)
1440
+ );
1441
+ }
1442
+ return row;
1443
+ },
1444
+
1445
+ getServerRecordInfo(server) {
1446
+ // Active, cached and indexed server records have evolved slightly over
1447
+ // time, so tolerate architecture and IP data in either documented location.
1448
+ var info = server.info || {};
1449
+ var os = info.os || {};
1450
+ return {
1451
+ ip: info.ip || server.ip || '',
1452
+ platform: os.platform || info.platform || '',
1453
+ distro: os.distro || '',
1454
+ release: os.release || info.release || '',
1455
+ arch: os.arch || info.arch || '',
1456
+ cpu: info.cpu || {},
1457
+ memory: info.memory || {},
1458
+ virt: info.virt || {},
1459
+ satellite: info.satellite || ''
1460
+ };
1461
+ },
1462
+
1463
+ getServerGroupSearchText(server) {
1464
+ return (server.groups || []).map( id => {
1465
+ var group = Tools.findObject(this.groups, { id: id });
1466
+ return id + ' ' + (group ? group.title : '');
1467
+ }).join(' ');
1468
+ },
1469
+
1470
+ getServerSearchText(server) {
1471
+ var info = this.getServerRecordInfo(server);
1472
+ return [
1473
+ server.id, server.title, server.hostname, info.ip,
1474
+ info.platform, info.distro, info.release, info.arch,
1475
+ info.cpu.brand, info.cpu.combo, info.virt.vendor,
1476
+ info.satellite, this.getServerGroupSearchText(server)
1477
+ ].filter( item => !!item ).join(' ').toLowerCase();
1478
+ },
1479
+
1480
+ parseServerBoolean(value, label) {
1481
+ if (typeof(value) != 'boolean') this.die('Server --' + label + ' must be true or false.');
1482
+ return value;
1483
+ },
1484
+
1485
+ parseServerList(value, label) {
1486
+ var values = Array.isArray(value) ? value.slice(0) : String(value == null ? '' : value).split(',');
1487
+ values = values.map( item => String(item).trim() ).filter( item => !!item );
1488
+ return Array.from(new Set(values));
1489
+ },
1490
+
1491
+ resolveServerGroup(selector) {
1492
+ var group = Tools.findObject(this.groups, { id: selector }) || this.findObjectFuzzy(this.groups, { title: selector });
1493
+ if (!group) this.die('Could not find server Group: ' + selector);
1494
+ return group.id;
1495
+ },
1496
+
1497
+ quoteServerQueryValue(value) {
1498
+ value = String(value);
1499
+ return value.match(/\s/) ? '"' + value.replace(/"/g, '\\"') + '"' : value;
1500
+ }
1501
+
1502
+ };