claw-dashboard 1.9.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.c8rc.json +38 -0
- package/.dockerignore +68 -0
- package/.github/dependabot.yml +45 -0
- package/.github/pull_request_template.md +39 -0
- package/.github/workflows/ci.yml +147 -0
- package/.github/workflows/release.yml +40 -0
- package/.github/workflows/security.yml +84 -0
- package/.husky/pre-commit +1 -0
- package/.planning/codebase/ARCHITECTURE.md +206 -0
- package/.planning/codebase/INTEGRATIONS.md +150 -0
- package/.planning/codebase/STACK.md +122 -0
- package/.planning/codebase/STRUCTURE.md +201 -0
- package/CHANGELOG.md +293 -0
- package/CONTRIBUTING.md +378 -0
- package/Dockerfile +54 -0
- package/FEATURES.md +195 -21
- package/README.md +83 -6
- package/TODO.md +28 -0
- package/build-cjs.js +127 -0
- package/cjs-shim.js +13 -0
- package/dist/clawdash +1729 -0
- package/dist/clawdash.meta.json +2236 -0
- package/dist/widgets.cjs +6511 -0
- package/docker-compose.yml +67 -0
- package/docs/API.md +1050 -0
- package/docs/PLUGINS.md +1504 -0
- package/esbuild.config.js +158 -0
- package/eslint.config.js +56 -0
- package/examples/plugins/README.md +122 -0
- package/examples/plugins/api-status/index.js +294 -0
- package/examples/plugins/api-status/plugin.json +19 -0
- package/examples/plugins/hello-world/index.js +104 -0
- package/examples/plugins/hello-world/plugin.json +15 -0
- package/examples/plugins/system-metrics-chart/index.js +339 -0
- package/examples/plugins/system-metrics-chart/plugin.json +18 -0
- package/examples/plugins/weather-widget/index.js +136 -0
- package/examples/plugins/weather-widget/plugin.json +19 -0
- package/index.cjs +23005 -0
- package/index.js +5285 -566
- package/jest.config.js +11 -0
- package/man/clawdash.1 +276 -0
- package/package.json +54 -6
- package/schemas/plugin-manifest.json +128 -0
- package/scripts/release.js +595 -0
- package/src/alerts.js +693 -0
- package/src/auto-save.js +584 -0
- package/src/cache.js +390 -0
- package/src/checksum.js +146 -0
- package/src/cli/args.js +110 -0
- package/src/cli/export-schedule.js +423 -0
- package/src/cli/export-snapshot.js +138 -0
- package/src/cli/help.js +69 -0
- package/src/cli/import-snapshot.js +230 -0
- package/src/cli/index.js +14 -0
- package/src/cli/list-templates.js +69 -0
- package/src/cli/validate-config.js +76 -0
- package/src/cli/validate-plugin.js +187 -0
- package/src/cli/version.js +15 -0
- package/src/config-validator.js +586 -0
- package/src/config-watcher.js +401 -0
- package/src/config.js +684 -0
- package/src/container-detector.js +499 -0
- package/src/database.js +734 -0
- package/src/differential-render.js +327 -0
- package/src/errors.js +169 -0
- package/src/export-scheduler.js +581 -0
- package/src/gateway-manager.js +685 -0
- package/src/hints.js +371 -0
- package/src/loading-states.js +509 -0
- package/src/logger.js +185 -0
- package/src/memory-pressure.js +467 -0
- package/src/performance-monitor.js +374 -0
- package/src/plugin-errors.js +586 -0
- package/src/plugin-manifest-validator.js +219 -0
- package/src/plugin-reload.js +481 -0
- package/src/plugin-scaffold.js +1934 -0
- package/src/plugin-validator.js +284 -0
- package/src/retry.js +218 -0
- package/src/security.js +860 -0
- package/src/snapshot.js +372 -0
- package/src/splash.js +115 -0
- package/src/theme-selector.js +411 -0
- package/src/themes.js +874 -0
- package/src/transitions.js +534 -0
- package/src/types.d.ts +174 -0
- package/src/validation.js +926 -0
- package/src/web-server.js +885 -0
- package/src/widgets/builtin-widgets.js +1056 -0
- package/src/widgets/config-processor.js +401 -0
- package/src/widgets/dependency-resolver.js +596 -0
- package/src/widgets/index.js +79 -0
- package/src/widgets/plugin-api.js +845 -0
- package/src/widgets/widget-error-boundary.js +773 -0
- package/src/widgets/widget-error-isolation.js +551 -0
- package/src/widgets/widget-loader.js +1437 -0
- package/src/workers/system-worker.js +130 -0
- package/src/workers/worker-pool.js +633 -0
- package/tests/alerts.test.js +437 -0
- package/tests/auto-save.test.js +529 -0
- package/tests/cache.test.js +317 -0
- package/tests/cli.test.js +351 -0
- package/tests/command-palette.test.js +320 -0
- package/tests/config-processor.test.js +452 -0
- package/tests/config-validator.test.js +710 -0
- package/tests/config-watcher.test.js +594 -0
- package/tests/config.test.js +755 -0
- package/tests/database.test.js +438 -0
- package/tests/errors.test.js +189 -0
- package/tests/example-plugins.test.js +624 -0
- package/tests/integration.test.js +1321 -0
- package/tests/loading-states.test.js +300 -0
- package/tests/manifest-validation-on-load.test.js +671 -0
- package/tests/performance-monitor.test.js +190 -0
- package/tests/plugin-api-rate-limit.test.js +302 -0
- package/tests/plugin-errors.test.js +311 -0
- package/tests/plugin-lifecycle-e2e.test.js +1036 -0
- package/tests/plugin-reload.test.js +764 -0
- package/tests/rate-limiter.test.js +539 -0
- package/tests/retry.test.js +308 -0
- package/tests/security.test.js +411 -0
- package/tests/settings-modal.test.js +226 -0
- package/tests/theme-selector.test.js +57 -0
- package/tests/utils.js +242 -0
- package/tests/utils.test.js +317 -0
- package/tests/validate-plugin-cli.test.js +359 -0
- package/tests/validation.test.js +837 -0
- package/tests/web-server.test.js +646 -0
- package/tests/widget-arrange-mode.test.js +183 -0
- package/tests/widget-config-hot-reload.test.js +465 -0
- package/tests/widget-dependency.test.js +591 -0
- package/tests/widget-error-boundary.test.js +749 -0
- package/tests/widget-error-isolation.test.js +469 -0
- package/tests/widget-integration.test.js +1147 -0
- package/tests/widget-refresh-intervals.test.js +284 -0
- package/tests/worker-pool.test.js +522 -0
|
@@ -0,0 +1,586 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Configuration Validator
|
|
3
|
+
* Validates dashboard-settings.json files
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { existsSync, readFileSync } from 'fs';
|
|
7
|
+
import { resolve, dirname } from 'path';
|
|
8
|
+
import {
|
|
9
|
+
VALIDATION,
|
|
10
|
+
GATEWAY,
|
|
11
|
+
WEB,
|
|
12
|
+
DEFAULT_SETTINGS,
|
|
13
|
+
PATHS
|
|
14
|
+
} from './config.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Validation result type
|
|
18
|
+
* @typedef {Object} ConfigValidationResult
|
|
19
|
+
* @property {boolean} valid - Whether the config is valid
|
|
20
|
+
* @property {string[]} errors - Array of error messages
|
|
21
|
+
* @property {string[]} warnings - Array of warning messages
|
|
22
|
+
* @property {string[]} info - Array of info messages
|
|
23
|
+
* @property {Object} stats - Validation statistics
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validate a config value type
|
|
28
|
+
* @private
|
|
29
|
+
* @param {*} value - The value to check
|
|
30
|
+
* @param {string} expectedType - Expected type
|
|
31
|
+
* @param {string} path - Path in config
|
|
32
|
+
* @returns {string|null} Error message or null
|
|
33
|
+
*/
|
|
34
|
+
function validateType(value, expectedType, path) {
|
|
35
|
+
if (value === undefined || value === null) {
|
|
36
|
+
return null; // Null/undefined handled separately
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const actualType = Array.isArray(value) ? 'array' : typeof value;
|
|
40
|
+
|
|
41
|
+
if (expectedType === 'integer') {
|
|
42
|
+
if (!Number.isInteger(value)) {
|
|
43
|
+
return `'${path}' must be an integer, got ${actualType}`;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (expectedType === 'port') {
|
|
49
|
+
if (!Number.isInteger(value) || value < 1 || value > 65535) {
|
|
50
|
+
return `'${path}' must be a valid port number (1-65535), got ${value}`;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (actualType !== expectedType) {
|
|
56
|
+
return `'${path}' must be of type ${expectedType}, got ${actualType}`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Validate gateway endpoint configuration
|
|
64
|
+
* @private
|
|
65
|
+
* @param {Object} endpoint - Endpoint config
|
|
66
|
+
* @param {number} index - Index in array
|
|
67
|
+
* @returns {Object} { errors: string[], warnings: string[] }
|
|
68
|
+
*/
|
|
69
|
+
function validateGatewayEndpoint(endpoint, index) {
|
|
70
|
+
const errors = [];
|
|
71
|
+
const warnings = [];
|
|
72
|
+
const path = `gatewayEndpoints[${index}]`;
|
|
73
|
+
|
|
74
|
+
if (!endpoint || typeof endpoint !== 'object') {
|
|
75
|
+
errors.push(`'${path}' must be an object`);
|
|
76
|
+
return { errors, warnings };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Required fields
|
|
80
|
+
if (!('name' in endpoint)) {
|
|
81
|
+
errors.push(`'${path}.name' is required`);
|
|
82
|
+
} else if (typeof endpoint.name === 'string') {
|
|
83
|
+
const nameLen = endpoint.name.length;
|
|
84
|
+
if (nameLen < VALIDATION.ENDPOINT_NAME.MIN_LENGTH) {
|
|
85
|
+
errors.push(`'${path}.name' must be at least ${VALIDATION.ENDPOINT_NAME.MIN_LENGTH} character(s)`);
|
|
86
|
+
}
|
|
87
|
+
if (nameLen > VALIDATION.ENDPOINT_NAME.MAX_LENGTH) {
|
|
88
|
+
errors.push(`'${path}.name' must be at most ${VALIDATION.ENDPOINT_NAME.MAX_LENGTH} characters`);
|
|
89
|
+
}
|
|
90
|
+
if (!VALIDATION.ENDPOINT_NAME.PATTERN.test(endpoint.name)) {
|
|
91
|
+
errors.push(`'${path}.name' must match pattern: ${VALIDATION.ENDPOINT_NAME.PATTERN.source}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!('host' in endpoint)) {
|
|
96
|
+
errors.push(`'${path}.host' is required`);
|
|
97
|
+
} else {
|
|
98
|
+
const hostError = validateType(endpoint.host, 'string', `${path}.host`);
|
|
99
|
+
if (hostError) errors.push(hostError);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!('port' in endpoint)) {
|
|
103
|
+
errors.push(`'${path}.port' is required`);
|
|
104
|
+
} else {
|
|
105
|
+
const portError = validateType(endpoint.port, 'port', `${path}.port`);
|
|
106
|
+
if (portError) errors.push(portError);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Optional fields
|
|
110
|
+
if ('enabled' in endpoint) {
|
|
111
|
+
const enabledError = validateType(endpoint.enabled, 'boolean', `${path}.enabled`);
|
|
112
|
+
if (enabledError) errors.push(enabledError);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if ('type' in endpoint) {
|
|
116
|
+
if (!VALIDATION.VALID_ENDPOINT_TYPES.includes(endpoint.type)) {
|
|
117
|
+
errors.push(`'${path}.type' must be one of: ${VALIDATION.VALID_ENDPOINT_TYPES.join(', ')}`);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if ('token' in endpoint && endpoint.token !== null) {
|
|
122
|
+
const tokenError = validateType(endpoint.token, 'string', `${path}.token`);
|
|
123
|
+
if (tokenError) errors.push(tokenError);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Warn about extra fields
|
|
127
|
+
const knownFields = ['name', 'host', 'port', 'enabled', 'type', 'token'];
|
|
128
|
+
const extraFields = Object.keys(endpoint).filter(k => !knownFields.includes(k));
|
|
129
|
+
for (const field of extraFields) {
|
|
130
|
+
warnings.push(`'${path}.${field}' is not a standard endpoint field`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return { errors, warnings };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Validate web interface configuration
|
|
138
|
+
* @private
|
|
139
|
+
* @param {Object} webConfig - Web interface config
|
|
140
|
+
* @returns {Object} { errors: string[], warnings: string[] }
|
|
141
|
+
*/
|
|
142
|
+
function validateWebInterfaceConfig(webConfig) {
|
|
143
|
+
const errors = [];
|
|
144
|
+
const warnings = [];
|
|
145
|
+
const path = 'webInterface';
|
|
146
|
+
|
|
147
|
+
if (!webConfig || typeof webConfig !== 'object') {
|
|
148
|
+
errors.push(`'${path}' must be an object`);
|
|
149
|
+
return { errors, warnings };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// enabled
|
|
153
|
+
if ('enabled' in webConfig) {
|
|
154
|
+
const err = validateType(webConfig.enabled, 'boolean', `${path}.enabled`);
|
|
155
|
+
if (err) errors.push(err);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// port
|
|
159
|
+
if ('port' in webConfig) {
|
|
160
|
+
const err = validateType(webConfig.port, 'port', `${path}.port`);
|
|
161
|
+
if (err) errors.push(err);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// host
|
|
165
|
+
if ('host' in webConfig) {
|
|
166
|
+
const err = validateType(webConfig.host, 'string', `${path}.host`);
|
|
167
|
+
if (err) errors.push(err);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// cors
|
|
171
|
+
if ('cors' in webConfig) {
|
|
172
|
+
const err = validateType(webConfig.cors, 'boolean', `${path}.cors`);
|
|
173
|
+
if (err) errors.push(err);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// corsOrigins - can be string or array
|
|
177
|
+
if ('corsOrigins' in webConfig) {
|
|
178
|
+
const origins = webConfig.corsOrigins;
|
|
179
|
+
if (typeof origins !== 'string' && !Array.isArray(origins)) {
|
|
180
|
+
errors.push(`'${path}.corsOrigins' must be a string or array`);
|
|
181
|
+
} else if (Array.isArray(origins)) {
|
|
182
|
+
for (let i = 0; i < origins.length; i++) {
|
|
183
|
+
if (typeof origins[i] !== 'string') {
|
|
184
|
+
errors.push(`'${path}.corsOrigins[${i}]' must be a string`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// rateLimit
|
|
191
|
+
if ('rateLimit' in webConfig) {
|
|
192
|
+
const rl = webConfig.rateLimit;
|
|
193
|
+
if (!rl || typeof rl !== 'object') {
|
|
194
|
+
errors.push(`'${path}.rateLimit' must be an object`);
|
|
195
|
+
} else {
|
|
196
|
+
if ('enabled' in rl) {
|
|
197
|
+
const err = validateType(rl.enabled, 'boolean', `${path}.rateLimit.enabled`);
|
|
198
|
+
if (err) errors.push(err);
|
|
199
|
+
}
|
|
200
|
+
if ('windowMs' in rl) {
|
|
201
|
+
const err = validateType(rl.windowMs, 'integer', `${path}.rateLimit.windowMs`);
|
|
202
|
+
if (err) errors.push(err);
|
|
203
|
+
else if (rl.windowMs < 1000) {
|
|
204
|
+
warnings.push(`'${path}.rateLimit.windowMs' is less than 1 second (${rl.windowMs}ms)`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if ('maxRequests' in rl) {
|
|
208
|
+
const err = validateType(rl.maxRequests, 'integer', `${path}.rateLimit.maxRequests`);
|
|
209
|
+
if (err) errors.push(err);
|
|
210
|
+
else if (rl.maxRequests < 1) {
|
|
211
|
+
errors.push(`'${path}.rateLimit.maxRequests' must be at least 1`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// auth
|
|
218
|
+
if ('auth' in webConfig) {
|
|
219
|
+
const auth = webConfig.auth;
|
|
220
|
+
if (!auth || typeof auth !== 'object') {
|
|
221
|
+
errors.push(`'${path}.auth' must be an object`);
|
|
222
|
+
} else {
|
|
223
|
+
if ('enabled' in auth) {
|
|
224
|
+
const err = validateType(auth.enabled, 'boolean', `${path}.auth.enabled`);
|
|
225
|
+
if (err) errors.push(err);
|
|
226
|
+
}
|
|
227
|
+
if ('keys' in auth) {
|
|
228
|
+
if (!Array.isArray(auth.keys)) {
|
|
229
|
+
errors.push(`'${path}.auth.keys' must be an array`);
|
|
230
|
+
} else {
|
|
231
|
+
for (let i = 0; i < auth.keys.length; i++) {
|
|
232
|
+
const key = auth.keys[i];
|
|
233
|
+
if (!key || typeof key !== 'object') {
|
|
234
|
+
errors.push(`'${path}.auth.keys[${i}]' must be an object`);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
return { errors, warnings };
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Validate widget loading configuration
|
|
247
|
+
* @private
|
|
248
|
+
* @param {Object} widgetConfig - Widget loading config
|
|
249
|
+
* @returns {Object} { errors: string[], warnings: string[] }
|
|
250
|
+
*/
|
|
251
|
+
function validateWidgetLoadingConfig(widgetConfig) {
|
|
252
|
+
const errors = [];
|
|
253
|
+
const warnings = [];
|
|
254
|
+
const path = 'widgetLoading';
|
|
255
|
+
|
|
256
|
+
if (!widgetConfig || typeof widgetConfig !== 'object') {
|
|
257
|
+
errors.push(`'${path}' must be an object`);
|
|
258
|
+
return { errors, warnings };
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// enabled
|
|
262
|
+
if ('enabled' in widgetConfig) {
|
|
263
|
+
const err = validateType(widgetConfig.enabled, 'boolean', `${path}.enabled`);
|
|
264
|
+
if (err) errors.push(err);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// preloadPriority
|
|
268
|
+
if ('preloadPriority' in widgetConfig) {
|
|
269
|
+
if (!Array.isArray(widgetConfig.preloadPriority)) {
|
|
270
|
+
errors.push(`'${path}.preloadPriority' must be an array`);
|
|
271
|
+
} else {
|
|
272
|
+
for (let i = 0; i < widgetConfig.preloadPriority.length; i++) {
|
|
273
|
+
if (typeof widgetConfig.preloadPriority[i] !== 'string') {
|
|
274
|
+
errors.push(`'${path}.preloadPriority[${i}]' must be a string`);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// lazyLoadDelay
|
|
281
|
+
if ('lazyLoadDelay' in widgetConfig) {
|
|
282
|
+
const err = validateType(widgetConfig.lazyLoadDelay, 'integer', `${path}.lazyLoadDelay`);
|
|
283
|
+
if (err) errors.push(err);
|
|
284
|
+
else if (widgetConfig.lazyLoadDelay < 0) {
|
|
285
|
+
errors.push(`'${path}.lazyLoadDelay' must be non-negative`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// maxConcurrent
|
|
290
|
+
if ('maxConcurrent' in widgetConfig) {
|
|
291
|
+
const err = validateType(widgetConfig.maxConcurrent, 'integer', `${path}.maxConcurrent`);
|
|
292
|
+
if (err) errors.push(err);
|
|
293
|
+
else if (widgetConfig.maxConcurrent < 1) {
|
|
294
|
+
errors.push(`'${path}.maxConcurrent' must be at least 1`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// autoDiscover
|
|
299
|
+
if ('autoDiscover' in widgetConfig) {
|
|
300
|
+
const err = validateType(widgetConfig.autoDiscover, 'boolean', `${path}.autoDiscover`);
|
|
301
|
+
if (err) errors.push(err);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return { errors, warnings };
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Validate a dashboard configuration object
|
|
309
|
+
* @param {Object} config - The parsed configuration object
|
|
310
|
+
* @param {Object} options - Validation options
|
|
311
|
+
* @param {boolean} options.strict - Whether to fail on unknown properties
|
|
312
|
+
* @returns {ConfigValidationResult} Validation result
|
|
313
|
+
*/
|
|
314
|
+
export function validateConfig(config, options = {}) {
|
|
315
|
+
const { strict = false } = options;
|
|
316
|
+
const errors = [];
|
|
317
|
+
const warnings = [];
|
|
318
|
+
const info = [];
|
|
319
|
+
|
|
320
|
+
if (!config || typeof config !== 'object') {
|
|
321
|
+
return {
|
|
322
|
+
valid: false,
|
|
323
|
+
errors: ['Config must be a valid JSON object'],
|
|
324
|
+
warnings: [],
|
|
325
|
+
info: [],
|
|
326
|
+
stats: { fieldCount: 0 }
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const fieldCount = Object.keys(config).length;
|
|
331
|
+
|
|
332
|
+
// Validate refreshInterval
|
|
333
|
+
if ('refreshInterval' in config) {
|
|
334
|
+
const err = validateType(config.refreshInterval, 'integer', 'refreshInterval');
|
|
335
|
+
if (err) errors.push(err);
|
|
336
|
+
else if (config.refreshInterval < VALIDATION.REFRESH_INTERVAL.MIN) {
|
|
337
|
+
errors.push(`'refreshInterval' must be at least ${VALIDATION.REFRESH_INTERVAL.MIN}ms`);
|
|
338
|
+
} else if (config.refreshInterval > VALIDATION.REFRESH_INTERVAL.MAX) {
|
|
339
|
+
errors.push(`'refreshInterval' must be at most ${VALIDATION.REFRESH_INTERVAL.MAX}ms`);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Check if value is in recommended options (1000, 2000, 5000, 10000ms)
|
|
343
|
+
const standardOptions = [1000, 2000, 5000, 10000];
|
|
344
|
+
if (!standardOptions.includes(config.refreshInterval)) {
|
|
345
|
+
const closest = standardOptions.reduce((prev, curr) =>
|
|
346
|
+
Math.abs(curr - config.refreshInterval) < Math.abs(prev - config.refreshInterval) ? curr : prev
|
|
347
|
+
);
|
|
348
|
+
info.push(`'refreshInterval' value ${config.refreshInterval}ms is not standard. Closest: ${closest}ms`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Validate logLevelFilter
|
|
353
|
+
if ('logLevelFilter' in config) {
|
|
354
|
+
if (!VALIDATION.VALID_LOG_LEVELS.includes(config.logLevelFilter)) {
|
|
355
|
+
errors.push(`'logLevelFilter' must be one of: ${VALIDATION.VALID_LOG_LEVELS.join(', ')}`);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Validate sessionSortMode
|
|
360
|
+
if ('sessionSortMode' in config) {
|
|
361
|
+
if (!VALIDATION.VALID_SORT_MODES.includes(config.sessionSortMode)) {
|
|
362
|
+
errors.push(`'sessionSortMode' must be one of: ${VALIDATION.VALID_SORT_MODES.join(', ')}`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Validate theme
|
|
367
|
+
if ('theme' in config) {
|
|
368
|
+
if (!VALIDATION.VALID_THEMES.includes(config.theme)) {
|
|
369
|
+
errors.push(`'theme' must be one of: ${VALIDATION.VALID_THEMES.join(', ')}`);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// Validate exportFormat
|
|
374
|
+
if ('exportFormat' in config) {
|
|
375
|
+
if (!VALIDATION.VALID_EXPORT_FORMATS.includes(config.exportFormat)) {
|
|
376
|
+
errors.push(`'exportFormat' must be one of: ${VALIDATION.VALID_EXPORT_FORMATS.join(', ')}`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Validate boolean showWidget fields
|
|
381
|
+
for (let i = 1; i <= 8; i++) {
|
|
382
|
+
const field = `showWidget${i}`;
|
|
383
|
+
if (field in config) {
|
|
384
|
+
const err = validateType(config[field], 'boolean', field);
|
|
385
|
+
if (err) errors.push(err);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Validate showPerformanceMetrics
|
|
390
|
+
if ('showPerformanceMetrics' in config) {
|
|
391
|
+
const err = validateType(config.showPerformanceMetrics, 'boolean', 'showPerformanceMetrics');
|
|
392
|
+
if (err) errors.push(err);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// Validate firstRun
|
|
396
|
+
if ('firstRun' in config) {
|
|
397
|
+
const err = validateType(config.firstRun, 'boolean', 'firstRun');
|
|
398
|
+
if (err) errors.push(err);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Validate showFavoritesOnly
|
|
402
|
+
if ('showFavoritesOnly' in config) {
|
|
403
|
+
const err = validateType(config.showFavoritesOnly, 'boolean', 'showFavoritesOnly');
|
|
404
|
+
if (err) errors.push(err);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// Validate favorites
|
|
408
|
+
if ('favorites' in config) {
|
|
409
|
+
if (!config.favorites || typeof config.favorites !== 'object') {
|
|
410
|
+
errors.push('\'favorites\' must be an object');
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Validate gatewayEndpoints
|
|
415
|
+
if ('gatewayEndpoints' in config) {
|
|
416
|
+
if (!Array.isArray(config.gatewayEndpoints)) {
|
|
417
|
+
errors.push('\'gatewayEndpoints\' must be an array');
|
|
418
|
+
} else {
|
|
419
|
+
if (config.gatewayEndpoints.length === 0) {
|
|
420
|
+
warnings.push('\'gatewayEndpoints\' is empty - no endpoints configured');
|
|
421
|
+
}
|
|
422
|
+
if (config.gatewayEndpoints.length > GATEWAY.MAX_ENDPOINTS) {
|
|
423
|
+
errors.push(`'gatewayEndpoints' exceeds maximum of ${GATEWAY.MAX_ENDPOINTS} endpoints`);
|
|
424
|
+
}
|
|
425
|
+
for (let i = 0; i < config.gatewayEndpoints.length; i++) {
|
|
426
|
+
const result = validateGatewayEndpoint(config.gatewayEndpoints[i], i);
|
|
427
|
+
errors.push(...result.errors);
|
|
428
|
+
warnings.push(...result.warnings);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// Validate activeGatewayEndpoint
|
|
434
|
+
if ('activeGatewayEndpoint' in config) {
|
|
435
|
+
const err = validateType(config.activeGatewayEndpoint, 'string', 'activeGatewayEndpoint');
|
|
436
|
+
if (err) errors.push(err);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// Validate webInterface
|
|
440
|
+
if ('webInterface' in config) {
|
|
441
|
+
const result = validateWebInterfaceConfig(config.webInterface);
|
|
442
|
+
errors.push(...result.errors);
|
|
443
|
+
warnings.push(...result.warnings);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Validate widgetLoading
|
|
447
|
+
if ('widgetLoading' in config) {
|
|
448
|
+
const result = validateWidgetLoadingConfig(config.widgetLoading);
|
|
449
|
+
errors.push(...result.errors);
|
|
450
|
+
warnings.push(...result.warnings);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Validate plugins
|
|
454
|
+
if ('plugins' in config) {
|
|
455
|
+
if (!config.plugins || typeof config.plugins !== 'object') {
|
|
456
|
+
errors.push('\'plugins\' must be an object');
|
|
457
|
+
} else {
|
|
458
|
+
const pluginCount = Object.keys(config.plugins).length;
|
|
459
|
+
if (pluginCount > 0) {
|
|
460
|
+
info.push(`Found configuration for ${pluginCount} plugin(s)`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// Validate exportDirectory
|
|
466
|
+
if ('exportDirectory' in config) {
|
|
467
|
+
const err = validateType(config.exportDirectory, 'string', 'exportDirectory');
|
|
468
|
+
if (err) errors.push(err);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Validate sessionSearchQuery
|
|
472
|
+
if ('sessionSearchQuery' in config) {
|
|
473
|
+
const err = validateType(config.sessionSearchQuery, 'string', 'sessionSearchQuery');
|
|
474
|
+
if (err) errors.push(err);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Check for unknown properties in strict mode
|
|
478
|
+
if (strict) {
|
|
479
|
+
const knownProps = Object.keys(DEFAULT_SETTINGS);
|
|
480
|
+
for (const key of Object.keys(config)) {
|
|
481
|
+
if (!knownProps.includes(key)) {
|
|
482
|
+
errors.push(`Unknown property: '${key}'`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return {
|
|
488
|
+
valid: errors.length === 0,
|
|
489
|
+
errors,
|
|
490
|
+
warnings,
|
|
491
|
+
info,
|
|
492
|
+
stats: { fieldCount }
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Validate a configuration file
|
|
498
|
+
* @param {string} filePath - Path to the configuration file
|
|
499
|
+
* @param {Object} options - Validation options
|
|
500
|
+
* @returns {ConfigValidationResult} Validation result
|
|
501
|
+
*/
|
|
502
|
+
export function validateConfigFile(filePath, options = {}) {
|
|
503
|
+
const resolvedPath = resolve(filePath);
|
|
504
|
+
|
|
505
|
+
if (!existsSync(resolvedPath)) {
|
|
506
|
+
return {
|
|
507
|
+
valid: false,
|
|
508
|
+
errors: [`File not found: ${resolvedPath}`],
|
|
509
|
+
warnings: [],
|
|
510
|
+
info: [],
|
|
511
|
+
stats: { fieldCount: 0 }
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
let config;
|
|
516
|
+
try {
|
|
517
|
+
const content = readFileSync(resolvedPath, 'utf8');
|
|
518
|
+
config = JSON.parse(content);
|
|
519
|
+
} catch (err) {
|
|
520
|
+
return {
|
|
521
|
+
valid: false,
|
|
522
|
+
errors: [`Failed to parse JSON: ${err.message}`],
|
|
523
|
+
warnings: [],
|
|
524
|
+
info: [],
|
|
525
|
+
stats: { fieldCount: 0 }
|
|
526
|
+
};
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
return validateConfig(config, options);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* Format validation results for display
|
|
534
|
+
* @param {ConfigValidationResult} result - The validation result
|
|
535
|
+
* @param {string} configPath - Optional config file path for context
|
|
536
|
+
* @returns {string} Formatted output
|
|
537
|
+
*/
|
|
538
|
+
export function formatConfigValidationResult(result, configPath = '') {
|
|
539
|
+
const lines = [];
|
|
540
|
+
const name = configPath ? ` ${configPath} ` : ' ';
|
|
541
|
+
|
|
542
|
+
if (result.valid) {
|
|
543
|
+
lines.push(`✓ Configuration${name}is valid`);
|
|
544
|
+
} else {
|
|
545
|
+
lines.push(`✗ Configuration${name}validation failed`);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (result.stats?.fieldCount !== undefined) {
|
|
549
|
+
lines.push(` ${result.stats.fieldCount} field(s) checked`);
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (result.errors.length > 0) {
|
|
553
|
+
lines.push('');
|
|
554
|
+
lines.push('Errors:');
|
|
555
|
+
result.errors.forEach(err => lines.push(` ✗ ${err}`));
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (result.warnings.length > 0) {
|
|
559
|
+
lines.push('');
|
|
560
|
+
lines.push('Warnings:');
|
|
561
|
+
result.warnings.forEach(warn => lines.push(` ⚠ ${warn}`));
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
if (result.info.length > 0) {
|
|
565
|
+
lines.push('');
|
|
566
|
+
lines.push('Info:');
|
|
567
|
+
result.info.forEach(i => lines.push(` ℹ ${i}`));
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
return lines.join('\n');
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Get default config path
|
|
575
|
+
* @returns {string} Default configuration file path
|
|
576
|
+
*/
|
|
577
|
+
export function getDefaultConfigPath() {
|
|
578
|
+
return PATHS.SETTINGS;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export default {
|
|
582
|
+
validateConfig,
|
|
583
|
+
validateConfigFile,
|
|
584
|
+
formatConfigValidationResult,
|
|
585
|
+
getDefaultConfigPath
|
|
586
|
+
};
|