claw-dashboard 1.9.0 → 2.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 (135) hide show
  1. package/.c8rc.json +38 -0
  2. package/.dockerignore +68 -0
  3. package/.github/dependabot.yml +45 -0
  4. package/.github/pull_request_template.md +39 -0
  5. package/.github/workflows/ci.yml +147 -0
  6. package/.github/workflows/release.yml +40 -0
  7. package/.github/workflows/security.yml +84 -0
  8. package/.husky/pre-commit +1 -0
  9. package/.planning/codebase/ARCHITECTURE.md +206 -0
  10. package/.planning/codebase/INTEGRATIONS.md +150 -0
  11. package/.planning/codebase/STACK.md +122 -0
  12. package/.planning/codebase/STRUCTURE.md +201 -0
  13. package/CHANGELOG.md +293 -0
  14. package/CONTRIBUTING.md +378 -0
  15. package/Dockerfile +54 -0
  16. package/FEATURES.md +195 -21
  17. package/README.md +83 -6
  18. package/TODO.md +28 -0
  19. package/build-cjs.js +127 -0
  20. package/cjs-shim.js +13 -0
  21. package/dist/clawdash +1729 -0
  22. package/dist/clawdash.meta.json +2236 -0
  23. package/dist/widgets.cjs +6511 -0
  24. package/docker-compose.yml +67 -0
  25. package/docs/API.md +1050 -0
  26. package/docs/PLUGINS.md +1504 -0
  27. package/esbuild.config.js +158 -0
  28. package/eslint.config.js +56 -0
  29. package/examples/plugins/README.md +122 -0
  30. package/examples/plugins/api-status/index.js +294 -0
  31. package/examples/plugins/api-status/plugin.json +19 -0
  32. package/examples/plugins/hello-world/index.js +104 -0
  33. package/examples/plugins/hello-world/plugin.json +15 -0
  34. package/examples/plugins/system-metrics-chart/index.js +339 -0
  35. package/examples/plugins/system-metrics-chart/plugin.json +18 -0
  36. package/examples/plugins/weather-widget/index.js +136 -0
  37. package/examples/plugins/weather-widget/plugin.json +19 -0
  38. package/index.cjs +23005 -0
  39. package/index.js +5236 -566
  40. package/jest.config.js +11 -0
  41. package/man/clawdash.1 +276 -0
  42. package/package.json +54 -5
  43. package/schemas/plugin-manifest.json +128 -0
  44. package/scripts/release.js +595 -0
  45. package/src/alerts.js +693 -0
  46. package/src/auto-save.js +584 -0
  47. package/src/cache.js +390 -0
  48. package/src/checksum.js +146 -0
  49. package/src/cli/args.js +110 -0
  50. package/src/cli/export-schedule.js +423 -0
  51. package/src/cli/export-snapshot.js +138 -0
  52. package/src/cli/help.js +69 -0
  53. package/src/cli/import-snapshot.js +230 -0
  54. package/src/cli/index.js +14 -0
  55. package/src/cli/list-templates.js +69 -0
  56. package/src/cli/validate-config.js +76 -0
  57. package/src/cli/validate-plugin.js +187 -0
  58. package/src/cli/version.js +15 -0
  59. package/src/config-validator.js +586 -0
  60. package/src/config-watcher.js +401 -0
  61. package/src/config.js +684 -0
  62. package/src/container-detector.js +499 -0
  63. package/src/database.js +734 -0
  64. package/src/differential-render.js +327 -0
  65. package/src/errors.js +169 -0
  66. package/src/export-scheduler.js +581 -0
  67. package/src/gateway-manager.js +685 -0
  68. package/src/hints.js +371 -0
  69. package/src/loading-states.js +509 -0
  70. package/src/logger.js +185 -0
  71. package/src/memory-pressure.js +467 -0
  72. package/src/performance-monitor.js +374 -0
  73. package/src/plugin-errors.js +586 -0
  74. package/src/plugin-manifest-validator.js +219 -0
  75. package/src/plugin-reload.js +481 -0
  76. package/src/plugin-scaffold.js +1941 -0
  77. package/src/plugin-validator.js +284 -0
  78. package/src/retry.js +218 -0
  79. package/src/security.js +860 -0
  80. package/src/snapshot.js +372 -0
  81. package/src/splash.js +115 -0
  82. package/src/theme-selector.js +411 -0
  83. package/src/themes.js +874 -0
  84. package/src/transitions.js +534 -0
  85. package/src/types.d.ts +174 -0
  86. package/src/validation.js +926 -0
  87. package/src/web-server.js +885 -0
  88. package/src/widgets/builtin-widgets.js +1057 -0
  89. package/src/widgets/config-processor.js +401 -0
  90. package/src/widgets/dependency-resolver.js +596 -0
  91. package/src/widgets/index.js +79 -0
  92. package/src/widgets/plugin-api.js +845 -0
  93. package/src/widgets/widget-error-boundary.js +773 -0
  94. package/src/widgets/widget-error-isolation.js +551 -0
  95. package/src/widgets/widget-loader.js +1437 -0
  96. package/src/workers/system-worker.js +130 -0
  97. package/src/workers/worker-pool.js +633 -0
  98. package/tests/alerts.test.js +437 -0
  99. package/tests/auto-save.test.js +529 -0
  100. package/tests/cache.test.js +317 -0
  101. package/tests/cli.test.js +351 -0
  102. package/tests/command-palette.test.js +320 -0
  103. package/tests/config-processor.test.js +452 -0
  104. package/tests/config-validator.test.js +710 -0
  105. package/tests/config-watcher.test.js +594 -0
  106. package/tests/config.test.js +755 -0
  107. package/tests/database.test.js +438 -0
  108. package/tests/errors.test.js +189 -0
  109. package/tests/example-plugins.test.js +624 -0
  110. package/tests/integration.test.js +1321 -0
  111. package/tests/loading-states.test.js +300 -0
  112. package/tests/manifest-validation-on-load.test.js +671 -0
  113. package/tests/performance-monitor.test.js +190 -0
  114. package/tests/plugin-api-rate-limit.test.js +302 -0
  115. package/tests/plugin-errors.test.js +311 -0
  116. package/tests/plugin-lifecycle-e2e.test.js +1036 -0
  117. package/tests/plugin-reload.test.js +764 -0
  118. package/tests/rate-limiter.test.js +539 -0
  119. package/tests/retry.test.js +308 -0
  120. package/tests/security.test.js +411 -0
  121. package/tests/settings-modal.test.js +226 -0
  122. package/tests/theme-selector.test.js +57 -0
  123. package/tests/utils.js +242 -0
  124. package/tests/utils.test.js +317 -0
  125. package/tests/validate-plugin-cli.test.js +359 -0
  126. package/tests/validation.test.js +837 -0
  127. package/tests/web-server.test.js +646 -0
  128. package/tests/widget-arrange-mode.test.js +183 -0
  129. package/tests/widget-config-hot-reload.test.js +465 -0
  130. package/tests/widget-dependency.test.js +591 -0
  131. package/tests/widget-error-boundary.test.js +749 -0
  132. package/tests/widget-error-isolation.test.js +469 -0
  133. package/tests/widget-integration.test.js +1147 -0
  134. package/tests/widget-refresh-intervals.test.js +284 -0
  135. package/tests/worker-pool.test.js +522 -0
@@ -0,0 +1,926 @@
1
+ /**
2
+ * Input validation module for settings and configuration values
3
+ * Validates all user inputs for settings like exportDirectory, theme, etc.
4
+ */
5
+
6
+ import logger from './logger.js';
7
+ import os from 'os';
8
+ import config, { EXPORT_SCHEDULE } from './config.js';
9
+ import fs from 'fs';
10
+ import { resolve, dirname } from 'path';
11
+ import { CronParser } from './export-scheduler.js';
12
+
13
+ // Valid option values
14
+ const VALID_THEMES = config.VALIDATION.VALID_THEMES;
15
+ const VALID_SORT_MODES = config.VALIDATION.VALID_SORT_MODES;
16
+ const VALID_LOG_LEVELS = config.VALIDATION.VALID_LOG_LEVELS;
17
+ const VALID_EXPORT_FORMATS = config.VALIDATION.VALID_EXPORT_FORMATS;
18
+
19
+ // Validation constraints
20
+ const CONSTRAINTS = {
21
+ refreshInterval: {
22
+ min: config.VALIDATION.REFRESH_INTERVAL.MIN,
23
+ max: config.VALIDATION.REFRESH_INTERVAL.MAX,
24
+ type: 'number'
25
+ },
26
+ logLevelFilter: {
27
+ type: 'string',
28
+ values: VALID_LOG_LEVELS
29
+ },
30
+ sessionSortMode: {
31
+ type: 'string',
32
+ values: VALID_SORT_MODES
33
+ },
34
+ theme: {
35
+ type: 'string',
36
+ values: VALID_THEMES
37
+ },
38
+ exportFormat: {
39
+ type: 'string',
40
+ values: VALID_EXPORT_FORMATS
41
+ },
42
+ exportDirectory: {
43
+ type: 'string',
44
+ required: false
45
+ }
46
+ };
47
+
48
+ /**
49
+ * Validate a file path
50
+ * @param {string} filePath - Path to validate
51
+ * @param {boolean} mustExist - Whether the path must exist
52
+ * @returns {object} Validation result
53
+ */
54
+ function validatePath(filePath, mustExist = false) {
55
+ if (!filePath || typeof filePath !== 'string') {
56
+ return { valid: false, error: 'Path must be a non-empty string' };
57
+ }
58
+
59
+ // Check for path traversal
60
+ if (filePath.includes('..')) {
61
+ return { valid: false, error: 'Path traversal not allowed' };
62
+ }
63
+
64
+ // Expand tilde
65
+ const expandedPath = filePath.startsWith('~')
66
+ ? resolve(os.homedir(), filePath.slice(1))
67
+ : resolve(filePath);
68
+
69
+ // Check if exists if required
70
+ if (mustExist && !fs.existsSync(expandedPath)) {
71
+ return { valid: false, error: `Path does not exist: ${expandedPath}` };
72
+ }
73
+
74
+ // Check if path is writable (directory exists or parent exists)
75
+ const parentDir = dirname(expandedPath);
76
+ if (!fs.existsSync(parentDir) && !fs.existsSync(expandedPath)) {
77
+ // Allow if parent can be created
78
+ try {
79
+ const parentExists = fs.existsSync(parentDir);
80
+ if (!parentExists) {
81
+ return { valid: true, resolvedPath: expandedPath, warning: 'Parent directory will be created' };
82
+ }
83
+ } catch {
84
+ return { valid: false, error: 'Cannot determine if path is writable' };
85
+ }
86
+ }
87
+
88
+ return { valid: true, resolvedPath: expandedPath };
89
+ }
90
+
91
+ /**
92
+ * Validate refresh interval
93
+ * @param {any} value - Value to validate
94
+ * @returns {object} Validation result
95
+ */
96
+ function validateRefreshInterval(value) {
97
+ if (value === undefined || value === null) {
98
+ return { valid: true, value: config.REFRESH_INTERVALS.DEFAULT }; // Default
99
+ }
100
+
101
+ const num = Number(value);
102
+
103
+ if (isNaN(num)) {
104
+ return { valid: false, error: 'Refresh interval must be a number' };
105
+ }
106
+
107
+ if (num < CONSTRAINTS.refreshInterval.min || num > CONSTRAINTS.refreshInterval.max) {
108
+ return {
109
+ valid: false,
110
+ error: `Refresh interval must be between ${CONSTRAINTS.refreshInterval.min}ms and ${CONSTRAINTS.refreshInterval.max}ms`
111
+ };
112
+ }
113
+
114
+ return { valid: true, value: num };
115
+ }
116
+
117
+ /**
118
+ * Validate log level filter
119
+ * @param {any} value - Value to validate
120
+ * @returns {object} Validation result
121
+ */
122
+ function validateLogLevelFilter(value) {
123
+ if (!value) {
124
+ return { valid: true, value: 'all' }; // Default
125
+ }
126
+
127
+ if (typeof value !== 'string') {
128
+ return { valid: false, error: 'Log level must be a string' };
129
+ }
130
+
131
+ const normalized = value.toLowerCase();
132
+
133
+ if (!CONSTRAINTS.logLevelFilter.values.includes(normalized)) {
134
+ return {
135
+ valid: false,
136
+ error: `Invalid log level. Must be one of: ${CONSTRAINTS.logLevelFilter.values.join(', ')}`
137
+ };
138
+ }
139
+
140
+ return { valid: true, value: normalized };
141
+ }
142
+
143
+ /**
144
+ * Validate session sort mode
145
+ * @param {any} value - Value to validate
146
+ * @returns {object} Validation result
147
+ */
148
+ function validateSessionSortMode(value) {
149
+ if (!value) {
150
+ return { valid: true, value: 'time' }; // Default
151
+ }
152
+
153
+ if (typeof value !== 'string') {
154
+ return { valid: false, error: 'Sort mode must be a string' };
155
+ }
156
+
157
+ const normalized = value.toLowerCase();
158
+
159
+ if (!CONSTRAINTS.sessionSortMode.values.includes(normalized)) {
160
+ return {
161
+ valid: false,
162
+ error: `Invalid sort mode. Must be one of: ${CONSTRAINTS.sessionSortMode.values.join(', ')}`
163
+ };
164
+ }
165
+
166
+ return { valid: true, value: normalized };
167
+ }
168
+
169
+ /**
170
+ * Validate theme
171
+ * @param {any} value - Value to validate
172
+ * @returns {object} Validation result
173
+ */
174
+ function validateTheme(value) {
175
+ if (!value) {
176
+ return { valid: true, value: 'default' }; // Default
177
+ }
178
+
179
+ if (typeof value !== 'string') {
180
+ return { valid: false, error: 'Theme must be a string' };
181
+ }
182
+
183
+ const normalized = value.toLowerCase();
184
+
185
+ if (!CONSTRAINTS.theme.values.includes(normalized)) {
186
+ return {
187
+ valid: false,
188
+ error: `Invalid theme. Must be one of: ${CONSTRAINTS.theme.values.join(', ')}`
189
+ };
190
+ }
191
+
192
+ return { valid: true, value: normalized };
193
+ }
194
+
195
+ /**
196
+ * Validate export format
197
+ * @param {any} value - Value to validate
198
+ * @returns {object} Validation result
199
+ */
200
+ function validateExportFormat(value) {
201
+ if (!value) {
202
+ return { valid: true, value: 'json' }; // Default
203
+ }
204
+
205
+ if (typeof value !== 'string') {
206
+ return { valid: false, error: 'Export format must be a string' };
207
+ }
208
+
209
+ const normalized = value.toLowerCase();
210
+
211
+ if (!CONSTRAINTS.exportFormat.values.includes(normalized)) {
212
+ return {
213
+ valid: false,
214
+ error: `Invalid export format. Must be one of: ${CONSTRAINTS.exportFormat.values.join(', ')}`
215
+ };
216
+ }
217
+
218
+ return { valid: true, value: normalized };
219
+ }
220
+
221
+ /**
222
+ * Validate export directory
223
+ * @param {any} value - Value to validate
224
+ * @returns {object} Validation result
225
+ */
226
+ function validateExportDirectory(value) {
227
+ if (!value) {
228
+ // Default directory
229
+ return { valid: true, value: config.PATHS.EXPORTS };
230
+ }
231
+
232
+ if (typeof value !== 'string') {
233
+ return { valid: false, error: 'Export directory must be a string' };
234
+ }
235
+
236
+ return validatePath(value, false);
237
+ }
238
+
239
+ /**
240
+ * Validate a boolean setting
241
+ * @param {any} value - Value to validate
242
+ * @param {string} name - Name of the setting for error messages
243
+ * @returns {object} Validation result
244
+ */
245
+ function validateBoolean(value, name = 'setting') {
246
+ if (value === undefined || value === null) {
247
+ return { valid: true, value: true }; // Default
248
+ }
249
+
250
+ if (typeof value !== 'boolean') {
251
+ // Try to parse string booleans
252
+ if (value === 'true' || value === '1' || value === 'yes') {
253
+ return { valid: true, value: true };
254
+ }
255
+ if (value === 'false' || value === '0' || value === 'no') {
256
+ return { valid: true, value: false };
257
+ }
258
+ return { valid: false, error: `${name} must be a boolean` };
259
+ }
260
+
261
+ return { valid: true, value: Boolean(value) };
262
+ }
263
+
264
+ /**
265
+ * Validate widget visibility (showWidget1-7)
266
+ * @param {any} value - Value to validate
267
+ * @returns {object} Validation result
268
+ */
269
+ function validateWidgetVisibility(value) {
270
+ return validateBoolean(value, 'Widget visibility');
271
+ }
272
+
273
+ /**
274
+ * Validate pinned widgets array
275
+ * @param {any} value - Value to validate (array of widget IDs)
276
+ * @returns {object} Validation result
277
+ */
278
+ function validatePinnedWidgets(value) {
279
+ if (!value) {
280
+ return { valid: true, value: [] };
281
+ }
282
+
283
+ if (!Array.isArray(value)) {
284
+ return { valid: false, error: 'pinnedWidgets must be an array' };
285
+ }
286
+
287
+ // Valid widget IDs that can be pinned
288
+ const validWidgetIds = ['cpu', 'mem', 'gpu', 'net', 'disk', 'sys', 'uptime', 'health', 'gateway'];
289
+
290
+ // Validate each widget ID
291
+ const validated = [];
292
+ for (const widgetId of value) {
293
+ if (typeof widgetId === 'string' && validWidgetIds.includes(widgetId)) {
294
+ validated.push(widgetId);
295
+ }
296
+ }
297
+
298
+ // Limit to max 4 pinned widgets
299
+ if (validated.length > 4) {
300
+ return { valid: true, value: validated.slice(0, 4), warning: 'Maximum 4 widgets can be pinned, truncating to first 4' };
301
+ }
302
+
303
+ return { valid: true, value: validated };
304
+ }
305
+
306
+ /**
307
+ * Validate widget order for drag-and-drop arrangement
308
+ * @param {any} value - Widget order array to validate
309
+ * @returns {object} Validation result
310
+ */
311
+ function validateWidgetOrder(value) {
312
+ if (!value) {
313
+ return { valid: true, value: [] };
314
+ }
315
+
316
+ if (!Array.isArray(value)) {
317
+ return { valid: false, error: 'widgetOrder must be an array' };
318
+ }
319
+
320
+ // Valid widget IDs
321
+ const validWidgetIds = ['cpu', 'mem', 'gpu', 'net', 'disk', 'sys', 'uptime', 'health', 'gateway'];
322
+
323
+ // Validate each widget ID and remove duplicates (keep first occurrence)
324
+ const validated = [];
325
+ const seen = new Set();
326
+ for (const widgetId of value) {
327
+ if (typeof widgetId === 'string' && validWidgetIds.includes(widgetId) && !seen.has(widgetId)) {
328
+ validated.push(widgetId);
329
+ seen.add(widgetId);
330
+ }
331
+ }
332
+
333
+ return { valid: true, value: validated };
334
+ }
335
+
336
+ /**
337
+ * Validate widget sizes configuration
338
+ * @param {any} widgetSizes - Widget sizes object to validate
339
+ * @returns {object} Validation result
340
+ */
341
+ function validateWidgetSizes(widgetSizes) {
342
+ if (!widgetSizes || typeof widgetSizes !== 'object') {
343
+ return { valid: true, value: {} };
344
+ }
345
+
346
+ const validSizes = ['small', 'medium', 'large', 'wide'];
347
+ const validWidgetIds = ['cpu', 'mem', 'gpu', 'net', 'disk', 'sys', 'uptime', 'health', 'gateway'];
348
+ const result = { valid: true, value: {} };
349
+
350
+ for (const [widgetId, size] of Object.entries(widgetSizes)) {
351
+ if (validWidgetIds.includes(widgetId)) {
352
+ if (validSizes.includes(size)) {
353
+ result.value[widgetId] = size;
354
+ }
355
+ // Invalid size is ignored (will use default)
356
+ }
357
+ }
358
+
359
+ return result;
360
+ }
361
+
362
+ /**
363
+ * Validate alert thresholds
364
+ * @param {any} thresholds - Thresholds object to validate
365
+ * @returns {object} Validation result
366
+ */
367
+ function validateAlertThresholds(thresholds) {
368
+ if (!thresholds || typeof thresholds !== 'object') {
369
+ return { valid: false, error: 'Alert thresholds must be an object' };
370
+ }
371
+
372
+ const result = { valid: true, value: {} };
373
+ const allowedTypes = ['cpu', 'memory', 'disk'];
374
+
375
+ for (const type of allowedTypes) {
376
+ if (thresholds[type]) {
377
+ const t = thresholds[type];
378
+
379
+ if (typeof t !== 'object') {
380
+ return { valid: false, error: `Alert threshold for ${type} must be an object` };
381
+ }
382
+
383
+ result.value[type] = {};
384
+
385
+ // Validate warning threshold
386
+ if (t.warning !== undefined) {
387
+ const warning = Number(t.warning);
388
+ if (isNaN(warning) || warning < 0 || warning > 100) {
389
+ return { valid: false, error: `${type} warning threshold must be 0-100` };
390
+ }
391
+ result.value[type].warning = warning;
392
+ } else {
393
+ result.value[type].warning = type === 'disk' ? 80 : 70; // Defaults
394
+ }
395
+
396
+ // Validate critical threshold
397
+ if (t.critical !== undefined) {
398
+ const critical = Number(t.critical);
399
+ if (isNaN(critical) || critical < 0 || critical > 100) {
400
+ return { valid: false, error: `${type} critical threshold must be 0-100` };
401
+ }
402
+ result.value[type].critical = critical;
403
+ } else {
404
+ result.value[type].critical = type === 'disk' ? 95 : 90; // Defaults
405
+ }
406
+
407
+ // Ensure critical >= warning
408
+ if (result.value[type].critical < result.value[type].warning) {
409
+ return { valid: false, error: `${type} critical threshold must be >= warning threshold` };
410
+ }
411
+ }
412
+ }
413
+
414
+ return result;
415
+ }
416
+
417
+ /**
418
+ * Validate auto-retry configuration
419
+ * @param {object} autoRetry - Auto-retry configuration to validate
420
+ * @returns {object} Validation result
421
+ */
422
+ function validateAutoRetry(autoRetry) {
423
+ if (!autoRetry || typeof autoRetry !== 'object') {
424
+ // Return defaults if not provided
425
+ return {
426
+ valid: true,
427
+ value: {
428
+ enabled: config.AUTO_RETRY.ENABLED,
429
+ intervalMs: config.AUTO_RETRY.DEFAULT_INTERVAL_MS,
430
+ exponentialBackoff: config.AUTO_RETRY.EXPONENTIAL_BACKOFF,
431
+ backoffMultiplier: config.AUTO_RETRY.BACKOFF_MULTIPLIER,
432
+ maxBackoffIntervalMs: config.AUTO_RETRY.MAX_BACKOFF_INTERVAL_MS,
433
+ resetAfterSuccess: config.AUTO_RETRY.RESET_AFTER_SUCCESS,
434
+ consecutiveFailureThreshold: config.AUTO_RETRY.CONSECUTIVE_FAILURE_THRESHOLD,
435
+ }
436
+ };
437
+ }
438
+
439
+ const validated = {};
440
+ const constraints = config.VALIDATION.AUTO_RETRY;
441
+
442
+ // Validate enabled (default: true)
443
+ validated.enabled = autoRetry.enabled !== false;
444
+
445
+ // Validate intervalMs
446
+ const interval = Number(autoRetry.intervalMs);
447
+ if (autoRetry.intervalMs !== undefined && (!isNaN(interval) && interval >= constraints.INTERVAL_MS.MIN && interval <= constraints.INTERVAL_MS.MAX)) {
448
+ validated.intervalMs = interval;
449
+ } else {
450
+ validated.intervalMs = config.AUTO_RETRY.DEFAULT_INTERVAL_MS;
451
+ }
452
+
453
+ // Validate exponentialBackoff (default: true)
454
+ validated.exponentialBackoff = autoRetry.exponentialBackoff !== false;
455
+
456
+ // Validate backoffMultiplier
457
+ const multiplier = Number(autoRetry.backoffMultiplier);
458
+ if (autoRetry.backoffMultiplier !== undefined && (!isNaN(multiplier) && multiplier >= constraints.BACKOFF_MULTIPLIER.MIN && multiplier <= constraints.BACKOFF_MULTIPLIER.MAX)) {
459
+ validated.backoffMultiplier = multiplier;
460
+ } else {
461
+ validated.backoffMultiplier = config.AUTO_RETRY.BACKOFF_MULTIPLIER;
462
+ }
463
+
464
+ // Validate maxBackoffIntervalMs
465
+ const maxBackoff = Number(autoRetry.maxBackoffIntervalMs);
466
+ if (autoRetry.maxBackoffIntervalMs !== undefined && (!isNaN(maxBackoff) && maxBackoff >= constraints.MAX_BACKOFF_INTERVAL_MS.MIN && maxBackoff <= constraints.MAX_BACKOFF_INTERVAL_MS.MAX)) {
467
+ validated.maxBackoffIntervalMs = maxBackoff;
468
+ } else {
469
+ validated.maxBackoffIntervalMs = config.AUTO_RETRY.MAX_BACKOFF_INTERVAL_MS;
470
+ }
471
+
472
+ // Validate resetAfterSuccess (default: true)
473
+ validated.resetAfterSuccess = autoRetry.resetAfterSuccess !== false;
474
+
475
+ // Validate consecutiveFailureThreshold
476
+ const threshold = Number(autoRetry.consecutiveFailureThreshold);
477
+ if (autoRetry.consecutiveFailureThreshold !== undefined && (!isNaN(threshold) && threshold >= constraints.CONSECUTIVE_FAILURE_THRESHOLD.MIN && threshold <= constraints.CONSECUTIVE_FAILURE_THRESHOLD.MAX)) {
478
+ validated.consecutiveFailureThreshold = threshold;
479
+ } else {
480
+ validated.consecutiveFailureThreshold = config.AUTO_RETRY.CONSECUTIVE_FAILURE_THRESHOLD;
481
+ }
482
+
483
+ return { valid: true, value: validated };
484
+ }
485
+
486
+ /**
487
+ * Validate auto-save configuration
488
+ * @param {object} autoSave - Auto-save configuration to validate
489
+ * @returns {object} Validation result
490
+ */
491
+ function validateAutoSave(autoSave) {
492
+ if (!autoSave || typeof autoSave !== 'object') {
493
+ // Return defaults if not provided
494
+ return {
495
+ valid: true,
496
+ value: {
497
+ enabled: config.AUTO_SAVE.ENABLED,
498
+ intervalMs: config.AUTO_SAVE.INTERVAL_MS,
499
+ saveOnExit: config.AUTO_SAVE.SAVE_ON_EXIT,
500
+ }
501
+ };
502
+ }
503
+
504
+ const validated = {};
505
+
506
+ // Validate enabled (default: true)
507
+ validated.enabled = autoSave.enabled !== false;
508
+
509
+ // Validate intervalMs (must be between 5s and 5min)
510
+ const interval = Number(autoSave.intervalMs);
511
+ if (!isNaN(interval) && interval >= 5000 && interval <= 300000) {
512
+ validated.intervalMs = interval;
513
+ } else {
514
+ validated.intervalMs = config.AUTO_SAVE.INTERVAL_MS;
515
+ }
516
+
517
+ // Validate saveOnExit (default: true)
518
+ validated.saveOnExit = autoSave.saveOnExit !== false;
519
+
520
+ return { valid: true, value: validated };
521
+ }
522
+
523
+ /**
524
+ * Validate export schedule configuration
525
+ * @param {object} exportSchedule - Export schedule configuration to validate
526
+ * @returns {object} Validation result
527
+ */
528
+ function validateExportSchedule(exportSchedule) {
529
+ if (!exportSchedule || typeof exportSchedule !== 'object') {
530
+ // Return defaults if not provided
531
+ return {
532
+ valid: true,
533
+ value: {
534
+ enabled: EXPORT_SCHEDULE.ENABLED,
535
+ format: EXPORT_SCHEDULE.DEFAULT_FORMAT,
536
+ schedule: EXPORT_SCHEDULE.DEFAULT_SCHEDULE,
537
+ retentionDays: EXPORT_SCHEDULE.DEFAULT_RETENTION_DAYS,
538
+ directory: null,
539
+ includeMetrics: true,
540
+ }
541
+ };
542
+ }
543
+
544
+ const validated = {};
545
+ const errors = [];
546
+
547
+ // Validate enabled (default: false)
548
+ validated.enabled = Boolean(exportSchedule.enabled);
549
+
550
+ // Validate format
551
+ if (exportSchedule.format !== undefined) {
552
+ if (['json', 'csv'].includes(exportSchedule.format)) {
553
+ validated.format = exportSchedule.format;
554
+ } else {
555
+ errors.push(`Invalid format: ${exportSchedule.format}`);
556
+ validated.format = EXPORT_SCHEDULE.DEFAULT_FORMAT;
557
+ }
558
+ } else {
559
+ validated.format = EXPORT_SCHEDULE.DEFAULT_FORMAT;
560
+ }
561
+
562
+ // Validate schedule (cron expression)
563
+ if (exportSchedule.schedule !== undefined) {
564
+ try {
565
+ CronParser.parse(exportSchedule.schedule);
566
+ validated.schedule = exportSchedule.schedule;
567
+ } catch (err) {
568
+ errors.push(`Invalid cron expression: ${exportSchedule.schedule}`);
569
+ validated.schedule = EXPORT_SCHEDULE.DEFAULT_SCHEDULE;
570
+ }
571
+ } else {
572
+ validated.schedule = EXPORT_SCHEDULE.DEFAULT_SCHEDULE;
573
+ }
574
+
575
+ // Validate retention days
576
+ if (exportSchedule.retentionDays !== undefined) {
577
+ const days = Number(exportSchedule.retentionDays);
578
+ if (!isNaN(days) && days >= EXPORT_SCHEDULE.MIN_RETENTION_DAYS && days <= EXPORT_SCHEDULE.MAX_RETENTION_DAYS) {
579
+ validated.retentionDays = days;
580
+ } else {
581
+ errors.push(`retentionDays must be ${EXPORT_SCHEDULE.MIN_RETENTION_DAYS}-${EXPORT_SCHEDULE.MAX_RETENTION_DAYS}`);
582
+ validated.retentionDays = EXPORT_SCHEDULE.DEFAULT_RETENTION_DAYS;
583
+ }
584
+ } else {
585
+ validated.retentionDays = EXPORT_SCHEDULE.DEFAULT_RETENTION_DAYS;
586
+ }
587
+
588
+ // Validate directory
589
+ if (exportSchedule.directory !== undefined && exportSchedule.directory !== null) {
590
+ if (typeof exportSchedule.directory === 'string') {
591
+ const pathResult = validatePath(exportSchedule.directory, false);
592
+ if (pathResult.valid) {
593
+ validated.directory = pathResult.resolvedPath;
594
+ } else {
595
+ errors.push(`Invalid directory: ${pathResult.error}`);
596
+ validated.directory = null;
597
+ }
598
+ } else {
599
+ errors.push('directory must be a string or null');
600
+ validated.directory = null;
601
+ }
602
+ } else {
603
+ validated.directory = null;
604
+ }
605
+
606
+ // Validate includeMetrics
607
+ validated.includeMetrics = exportSchedule.includeMetrics !== false;
608
+
609
+ if (errors.length > 0) {
610
+ logger.warn(`Export schedule validation warnings: ${errors.join('; ')}`);
611
+ }
612
+
613
+ return { valid: true, value: validated };
614
+ }
615
+
616
+ /**
617
+ * Validate all settings at once
618
+ * @param {object} settings - Settings object to validate
619
+ * @returns {object} Validation result with validated settings
620
+ */
621
+ function validateSettings(settings) {
622
+ if (!settings || typeof settings !== 'object') {
623
+ logger.warn('Settings must be an object, using defaults');
624
+ return getDefaultSettings();
625
+ }
626
+
627
+ const validated = {};
628
+ const errors = [];
629
+
630
+ // Validate each setting
631
+ const validators = {
632
+ refreshInterval: validateRefreshInterval,
633
+ logLevelFilter: validateLogLevelFilter,
634
+ sessionSortMode: validateSessionSortMode,
635
+ theme: validateTheme,
636
+ exportFormat: validateExportFormat,
637
+ exportDirectory: validateExportDirectory,
638
+ showWidget1: validateWidgetVisibility,
639
+ showWidget2: validateWidgetVisibility,
640
+ showWidget3: validateWidgetVisibility,
641
+ showWidget4: validateWidgetVisibility,
642
+ showWidget5: validateWidgetVisibility,
643
+ showWidget6: validateWidgetVisibility,
644
+ showWidget7: validateWidgetVisibility,
645
+ pinnedWidgets: validatePinnedWidgets,
646
+ widgetOrder: validateWidgetOrder,
647
+ widgetSizes: validateWidgetSizes,
648
+ exportSchedule: validateExportSchedule,
649
+ };
650
+
651
+ for (const [key, validator] of Object.entries(validators)) {
652
+ const result = validator(settings[key]);
653
+ if (result.valid) {
654
+ validated[key] = result.value;
655
+ } else {
656
+ errors.push(`${key}: ${result.error}`);
657
+ // Use default value
658
+ validated[key] = getDefaultValue(key);
659
+ }
660
+ }
661
+
662
+ // Validate autoRetry configuration separately
663
+ const autoRetryResult = validateAutoRetry(settings.autoRetry);
664
+ if (autoRetryResult.valid) {
665
+ validated.autoRetry = autoRetryResult.value;
666
+ } else {
667
+ errors.push(`autoRetry: ${autoRetryResult.error}`);
668
+ validated.autoRetry = autoRetryResult.value; // Uses defaults
669
+ }
670
+
671
+ // Validate autoSave configuration separately
672
+ const autoSaveResult = validateAutoSave(settings.autoSave);
673
+ if (autoSaveResult.valid) {
674
+ validated.autoSave = autoSaveResult.value;
675
+ } else {
676
+ errors.push(`autoSave: ${autoSaveResult.error}`);
677
+ validated.autoSave = autoSaveResult.value; // Uses defaults
678
+ }
679
+
680
+ // Validate exportSchedule configuration separately
681
+ const exportScheduleResult = validateExportSchedule(settings.exportSchedule);
682
+ if (exportScheduleResult.valid) {
683
+ validated.exportSchedule = exportScheduleResult.value;
684
+ } else {
685
+ errors.push(`exportSchedule: ${exportScheduleResult.error}`);
686
+ validated.exportSchedule = exportScheduleResult.value; // Uses defaults
687
+ }
688
+
689
+ if (errors.length > 0) {
690
+ logger.warn(`Settings validation errors: ${errors.join('; ')}`);
691
+ }
692
+
693
+ return { valid: true, value: validated };
694
+ }
695
+
696
+ /**
697
+ * Get default value for a setting
698
+ * @param {string} key - Setting key
699
+ * @returns {any} Default value
700
+ */
701
+ function getDefaultValue(key) {
702
+ const defaults = {
703
+ refreshInterval: config.REFRESH_INTERVALS.DEFAULT,
704
+ logLevelFilter: 'all',
705
+ sessionSortMode: 'time',
706
+ theme: 'default',
707
+ exportFormat: 'json',
708
+ exportDirectory: config.PATHS.EXPORTS,
709
+ autoRetry: {
710
+ enabled: config.AUTO_RETRY.ENABLED,
711
+ intervalMs: config.AUTO_RETRY.DEFAULT_INTERVAL_MS,
712
+ exponentialBackoff: config.AUTO_RETRY.EXPONENTIAL_BACKOFF,
713
+ backoffMultiplier: config.AUTO_RETRY.BACKOFF_MULTIPLIER,
714
+ maxBackoffIntervalMs: config.AUTO_RETRY.MAX_BACKOFF_INTERVAL_MS,
715
+ resetAfterSuccess: config.AUTO_RETRY.RESET_AFTER_SUCCESS,
716
+ consecutiveFailureThreshold: config.AUTO_RETRY.CONSECUTIVE_FAILURE_THRESHOLD,
717
+ },
718
+ showWidget1: true,
719
+ showWidget2: true,
720
+ showWidget3: true,
721
+ showWidget4: true,
722
+ showWidget5: true,
723
+ showWidget6: true,
724
+ showWidget7: true,
725
+ pinnedWidgets: [],
726
+ widgetOrder: [],
727
+ exportSchedule: {
728
+ enabled: EXPORT_SCHEDULE.ENABLED,
729
+ format: EXPORT_SCHEDULE.DEFAULT_FORMAT,
730
+ schedule: EXPORT_SCHEDULE.DEFAULT_SCHEDULE,
731
+ retentionDays: EXPORT_SCHEDULE.DEFAULT_RETENTION_DAYS,
732
+ directory: null,
733
+ includeMetrics: true,
734
+ },
735
+ };
736
+ return defaults[key];
737
+ }
738
+
739
+ /**
740
+ * Get all default settings
741
+ * @returns {object} Default settings object
742
+ */
743
+ function getDefaultSettings() {
744
+ return {
745
+ refreshInterval: config.REFRESH_INTERVALS.DEFAULT,
746
+ versionCheckInterval: 43200000, // 12 hours in milliseconds
747
+ lastVersionCheck: 0,
748
+ logLevelFilter: 'all',
749
+ sessionSortMode: 'time',
750
+ theme: 'default',
751
+ exportFormat: 'json',
752
+ exportDirectory: config.PATHS.EXPORTS,
753
+ showWidget1: true,
754
+ showWidget2: true,
755
+ showWidget3: true,
756
+ showWidget4: true,
757
+ showWidget5: true,
758
+ showWidget6: true,
759
+ showWidget7: true,
760
+ showWidget8: true,
761
+ showPerformanceMetrics: false,
762
+ sessionSearchQuery: '',
763
+ favorites: {},
764
+ showFavoritesOnly: false,
765
+ pinnedWidgets: [],
766
+ widgetOrder: [],
767
+ widgetSizes: {
768
+ cpu: 'medium',
769
+ mem: 'medium',
770
+ gpu: 'medium',
771
+ net: 'medium',
772
+ disk: 'medium',
773
+ sys: 'medium',
774
+ uptime: 'medium',
775
+ health: 'medium',
776
+ gateway: 'medium',
777
+ },
778
+ firstRun: true,
779
+ gatewayEndpoints: [{
780
+ name: 'local',
781
+ host: 'localhost',
782
+ port: 18789,
783
+ token: null,
784
+ enabled: true,
785
+ type: 'local'
786
+ }],
787
+ activeGatewayEndpoint: 'local',
788
+ webInterface: {
789
+ enabled: false,
790
+ port: config.WEB.DEFAULT_PORT,
791
+ host: config.WEB.HOST,
792
+ cors: true
793
+ }
794
+ };
795
+ }
796
+
797
+ /**
798
+ * Validate gateway endpoint configuration
799
+ * @param {object} endpoint - Endpoint configuration to validate
800
+ * @returns {object} Validation result
801
+ */
802
+ function validateGatewayEndpoint(endpoint) {
803
+ if (!endpoint || typeof endpoint !== 'object') {
804
+ return { valid: false, error: 'Endpoint must be an object' };
805
+ }
806
+
807
+ // Validate name
808
+ if (!endpoint.name || typeof endpoint.name !== 'string' || endpoint.name.length === 0) {
809
+ return { valid: false, error: 'Endpoint name is required and must be a non-empty string' };
810
+ }
811
+
812
+ if (endpoint.name.length > config.VALIDATION.ENDPOINT_NAME.MAX_LENGTH) {
813
+ return { valid: false, error: `Endpoint name must be at most ${config.VALIDATION.ENDPOINT_NAME.MAX_LENGTH} characters` };
814
+ }
815
+
816
+ if (!config.VALIDATION.ENDPOINT_NAME.PATTERN.test(endpoint.name)) {
817
+ return { valid: false, error: 'Endpoint name must contain only alphanumeric characters, underscores, and hyphens' };
818
+ }
819
+
820
+ // Validate host
821
+ if (!endpoint.host || typeof endpoint.host !== 'string' || endpoint.host.length === 0) {
822
+ return { valid: false, error: 'Endpoint host is required and must be a non-empty string' };
823
+ }
824
+
825
+ // Validate port
826
+ const port = Number(endpoint.port);
827
+ if (isNaN(port) || port < 1 || port > 65535) {
828
+ return { valid: false, error: 'Endpoint port must be a valid port number (1-65535)' };
829
+ }
830
+
831
+ // Validate type if provided
832
+ if (endpoint.type !== undefined) {
833
+ if (!config.VALIDATION.VALID_ENDPOINT_TYPES.includes(endpoint.type)) {
834
+ return { valid: false, error: `Endpoint type must be one of: ${config.VALIDATION.VALID_ENDPOINT_TYPES.join(', ')}` };
835
+ }
836
+ }
837
+
838
+ // Validate enabled if provided (should be boolean)
839
+ if (endpoint.enabled !== undefined && typeof endpoint.enabled !== 'boolean') {
840
+ return { valid: false, error: 'Endpoint enabled must be a boolean' };
841
+ }
842
+
843
+ // Validate token if provided (should be string or null)
844
+ if (endpoint.token !== undefined && endpoint.token !== null && typeof endpoint.token !== 'string') {
845
+ return { valid: false, error: 'Endpoint token must be a string or null' };
846
+ }
847
+
848
+ return {
849
+ valid: true,
850
+ value: {
851
+ name: endpoint.name,
852
+ host: endpoint.host,
853
+ port: port,
854
+ token: endpoint.token || null,
855
+ enabled: endpoint.enabled !== false, // default true
856
+ type: endpoint.type || 'local'
857
+ }
858
+ };
859
+ }
860
+
861
+ /**
862
+ * Validate a single value against a type
863
+ * @param {any} value - Value to validate
864
+ * @param {string} type - Expected type
865
+ * @returns {boolean} Whether valid
866
+ */
867
+ function validateType(value, type) {
868
+ switch (type) {
869
+ case 'number':
870
+ return typeof value === 'number' && !isNaN(value);
871
+ case 'string':
872
+ return typeof value === 'string';
873
+ case 'boolean':
874
+ return typeof value === 'boolean';
875
+ case 'object':
876
+ return typeof value === 'object' && value !== null;
877
+ default:
878
+ return false;
879
+ }
880
+ }
881
+
882
+ export {
883
+ validateSettings,
884
+ validateRefreshInterval,
885
+ validateLogLevelFilter,
886
+ validateSessionSortMode,
887
+ validateTheme,
888
+ validateExportFormat,
889
+ validateExportDirectory,
890
+ validateWidgetVisibility,
891
+ validateAlertThresholds,
892
+ validateAutoRetry,
893
+ validateAutoSave,
894
+ validateExportSchedule,
895
+ validatePath,
896
+ validateType,
897
+ validateGatewayEndpoint,
898
+ getDefaultSettings,
899
+ VALID_THEMES,
900
+ VALID_SORT_MODES,
901
+ VALID_LOG_LEVELS,
902
+ VALID_EXPORT_FORMATS
903
+ };
904
+
905
+ export default {
906
+ validateSettings,
907
+ validateRefreshInterval,
908
+ validateLogLevelFilter,
909
+ validateSessionSortMode,
910
+ validateTheme,
911
+ validateExportFormat,
912
+ validateExportDirectory,
913
+ validateWidgetVisibility,
914
+ validateAlertThresholds,
915
+ validateAutoRetry,
916
+ validateAutoSave,
917
+ validateExportSchedule,
918
+ validatePath,
919
+ validateType,
920
+ validateGatewayEndpoint,
921
+ getDefaultSettings,
922
+ VALID_THEMES,
923
+ VALID_SORT_MODES,
924
+ VALID_LOG_LEVELS,
925
+ VALID_EXPORT_FORMATS
926
+ };