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,401 @@
1
+ /**
2
+ * Widget Configuration Processor
3
+ * Handles environment variable interpolation, config versioning, and migration
4
+ */
5
+
6
+ import logger from '../logger.js';
7
+
8
+ /**
9
+ * Default configuration processing options
10
+ */
11
+ export const DEFAULT_PROCESSING_OPTIONS = {
12
+ interpolateEnv: true,
13
+ supportLegacy: true,
14
+ validateVersion: true,
15
+ throwOnError: false,
16
+ };
17
+
18
+ /**
19
+ * Interpolate environment variables in config values
20
+ * Supports ${ENV_VAR} and ${ENV_VAR:-default} syntax
21
+ *
22
+ * @param {string} value - String value to interpolate
23
+ * @param {Object} env - Environment variables object (defaults to process.env)
24
+ * @returns {string} Interpolated value
25
+ */
26
+ export function interpolateEnvVars(value, env = process.env) {
27
+ if (typeof value !== 'string') {
28
+ return value;
29
+ }
30
+
31
+ // Match ${VAR} or ${VAR:-default} patterns
32
+ const pattern = /\$\{([^}]+)\}/g;
33
+
34
+ return value.replace(pattern, (match, content) => {
35
+ // Check for default value syntax: VAR:-default
36
+ const colonIndex = content.indexOf(':-');
37
+
38
+ if (colonIndex !== -1) {
39
+ const varName = content.substring(0, colonIndex);
40
+ const defaultValue = content.substring(colonIndex + 2);
41
+ return env[varName] !== undefined ? env[varName] : defaultValue;
42
+ }
43
+
44
+ // Simple variable substitution
45
+ return env[content] !== undefined ? env[content] : match;
46
+ });
47
+ }
48
+
49
+ /**
50
+ * Deep process config object, interpolating env vars in all string values
51
+ *
52
+ * @param {*} config - Config value (any type)
53
+ * @param {Object} env - Environment variables
54
+ * @param {Set} visited - Track visited objects to prevent circular reference issues
55
+ * @returns {*} Processed config
56
+ */
57
+ export function processConfigValues(config, env = process.env, visited = new Set()) {
58
+ // Handle null/undefined
59
+ if (config === null || config === undefined) {
60
+ return config;
61
+ }
62
+
63
+ // Handle strings - interpolate env vars
64
+ if (typeof config === 'string') {
65
+ return interpolateEnvVars(config, env);
66
+ }
67
+
68
+ // Handle arrays
69
+ if (Array.isArray(config)) {
70
+ return config.map(item => processConfigValues(item, env, visited));
71
+ }
72
+
73
+ // Handle objects (but not dates, regexps, etc)
74
+ if (typeof config === 'object' && config.constructor === Object) {
75
+ // Prevent circular references
76
+ if (visited.has(config)) {
77
+ logger.warn('Circular reference detected in config, skipping');
78
+ return config;
79
+ }
80
+ visited.add(config);
81
+
82
+ const result = {};
83
+ for (const [key, value] of Object.entries(config)) {
84
+ result[key] = processConfigValues(value, env, visited);
85
+ }
86
+ visited.delete(config);
87
+ return result;
88
+ }
89
+
90
+ // Return primitives as-is
91
+ return config;
92
+ }
93
+
94
+ /**
95
+ * Config version information
96
+ */
97
+ export const CONFIG_VERSION = {
98
+ CURRENT: '1.0.0',
99
+ MIN_SUPPORTED: '1.0.0',
100
+ };
101
+
102
+ /**
103
+ * Parse version string to comparable array
104
+ * @param {string} version - Semver version string
105
+ * @returns {number[]} [major, minor, patch]
106
+ */
107
+ function parseVersion(version) {
108
+ const parts = version.split('.').map(Number);
109
+ return [parts[0] || 0, parts[1] || 0, parts[2] || 0];
110
+ }
111
+
112
+ /**
113
+ * Compare two versions
114
+ * @param {string} v1 - First version
115
+ * @param {string} v2 - Second version
116
+ * @returns {number} -1 if v1 < v2, 0 if equal, 1 if v1 > v2
117
+ */
118
+ export function compareVersions(v1, v2) {
119
+ const a = parseVersion(v1);
120
+ const b = parseVersion(v2);
121
+
122
+ for (let i = 0; i < 3; i++) {
123
+ if (a[i] < b[i]) return -1;
124
+ if (a[i] > b[i]) return 1;
125
+ }
126
+ return 0;
127
+ }
128
+
129
+ /**
130
+ * Config migration registry
131
+ * Maps version ranges to migration functions
132
+ */
133
+ const migrations = new Map();
134
+
135
+ /**
136
+ * Register a config migration
137
+ * @param {string} fromVersion - Source version (exact match or 'x.x.x' for any)
138
+ * @param {string} toVersion - Target version
139
+ * @param {Function} migrateFn - Migration function: (config) => migratedConfig
140
+ */
141
+ export function registerMigration(fromVersion, toVersion, migrateFn) {
142
+ const key = `${fromVersion}→${toVersion}`;
143
+ migrations.set(key, {
144
+ fromVersion,
145
+ toVersion,
146
+ migrate: migrateFn,
147
+ });
148
+ logger.debug(`Registered config migration: ${key}`);
149
+ }
150
+
151
+ /**
152
+ * Find migration path from source to target version
153
+ * Uses simple greedy algorithm - for complex cases, explicit migration chains should be registered
154
+ *
155
+ * @param {string} fromVersion - Starting version
156
+ * @param {string} toVersion - Target version
157
+ * @returns {Array} Array of migration steps
158
+ */
159
+ function findMigrationPath(fromVersion, toVersion) {
160
+ // Same version - no migration needed
161
+ if (fromVersion === toVersion) {
162
+ return [];
163
+ }
164
+
165
+ // Look for direct migration
166
+ const directKey = `${fromVersion}→${toVersion}`;
167
+ if (migrations.has(directKey)) {
168
+ return [migrations.get(directKey)];
169
+ }
170
+
171
+ // Look for wildcard migration from this version
172
+ for (const [key, migration] of migrations) {
173
+ if (migration.fromVersion === fromVersion) {
174
+ // Found a step, recursively find rest of path
175
+ const remainingPath = findMigrationPath(migration.toVersion, toVersion);
176
+ if (remainingPath !== null) {
177
+ return [migration, ...remainingPath];
178
+ }
179
+ }
180
+ }
181
+
182
+ // No migration path found
183
+ return null;
184
+ }
185
+
186
+ /**
187
+ * Validate config version against supported range
188
+ * @param {Object} config - Widget configuration
189
+ * @returns {Object} Validation result { valid: boolean, error?: string, migrated?: boolean }
190
+ */
191
+ export function validateConfigVersion(config) {
192
+ const configVersion = config?.__version || '1.0.0';
193
+
194
+ // Check minimum supported version
195
+ if (compareVersions(configVersion, CONFIG_VERSION.MIN_SUPPORTED) < 0) {
196
+ return {
197
+ valid: false,
198
+ error: `Config version ${configVersion} is below minimum supported ${CONFIG_VERSION.MIN_SUPPORTED}`,
199
+ };
200
+ }
201
+
202
+ // Check maximum (future) version
203
+ if (compareVersions(configVersion, CONFIG_VERSION.CURRENT) > 0) {
204
+ return {
205
+ valid: false,
206
+ error: `Config version ${configVersion} is newer than current ${CONFIG_VERSION.CURRENT}. Please upgrade the dashboard.`,
207
+ };
208
+ }
209
+
210
+ return { valid: true, version: configVersion };
211
+ }
212
+
213
+ /**
214
+ * Migrate config to current version
215
+ * @param {Object} config - Widget configuration
216
+ * @param {string} targetVersion - Target version (defaults to CURRENT)
217
+ * @returns {Object} Migration result { success: boolean, config?: Object, error?: string, path?: string[] }
218
+ */
219
+ export function migrateConfig(config, targetVersion = CONFIG_VERSION.CURRENT) {
220
+ if (!config || typeof config !== 'object') {
221
+ return { success: false, error: 'Invalid config object' };
222
+ }
223
+
224
+ const sourceVersion = config.__version || '1.0.0';
225
+
226
+ // No migration needed
227
+ if (sourceVersion === targetVersion) {
228
+ return { success: true, config, path: [] };
229
+ }
230
+
231
+ // Find migration path
232
+ const migrationPath = findMigrationPath(sourceVersion, targetVersion);
233
+
234
+ if (migrationPath === null) {
235
+ return {
236
+ success: false,
237
+ error: `No migration path from ${sourceVersion} to ${targetVersion}`,
238
+ };
239
+ }
240
+
241
+ // Apply migrations
242
+ let migratedConfig = { ...config };
243
+ const path = [];
244
+
245
+ try {
246
+ for (const migration of migrationPath) {
247
+ migratedConfig = migration.migrate(migratedConfig);
248
+ migratedConfig.__version = migration.toVersion;
249
+ path.push(`${migration.fromVersion}→${migration.toVersion}`);
250
+ }
251
+
252
+ return {
253
+ success: true,
254
+ config: migratedConfig,
255
+ path,
256
+ };
257
+ } catch (err) {
258
+ return {
259
+ success: false,
260
+ error: `Migration failed: ${err.message}`,
261
+ path,
262
+ };
263
+ }
264
+ }
265
+
266
+ /**
267
+ * Process widget configuration
268
+ * Applies env interpolation, version validation, and migration
269
+ *
270
+ * @param {Object} config - Raw widget configuration
271
+ * @param {Object} options - Processing options
272
+ * @returns {Object} Processing result { success: boolean, config?: Object, error?: string, warnings?: string[] }
273
+ */
274
+ export function processWidgetConfig(config, options = {}) {
275
+ const opts = { ...DEFAULT_PROCESSING_OPTIONS, ...options };
276
+ const warnings = [];
277
+
278
+ try {
279
+ let processedConfig = config;
280
+
281
+ // Validate and migrate version
282
+ if (opts.validateVersion) {
283
+ const validation = validateConfigVersion(processedConfig);
284
+ if (!validation.valid) {
285
+ // Try to migrate if version is old
286
+ if (validation.error?.includes('below minimum')) {
287
+ if (opts.throwOnError) {
288
+ throw new Error(validation.error);
289
+ }
290
+ return { success: false, error: validation.error };
291
+ }
292
+
293
+ // Version is newer than current - can't migrate forward
294
+ if (opts.throwOnError) {
295
+ throw new Error(validation.error);
296
+ }
297
+ return { success: false, error: validation.error };
298
+ }
299
+
300
+ // Migrate if needed
301
+ if (validation.version !== CONFIG_VERSION.CURRENT) {
302
+ const migration = migrateConfig(processedConfig);
303
+ if (!migration.success) {
304
+ if (opts.throwOnError) {
305
+ throw new Error(migration.error);
306
+ }
307
+ warnings.push(`Config migration failed: ${migration.error}`);
308
+ } else {
309
+ processedConfig = migration.config;
310
+ if (migration.path?.length > 0) {
311
+ warnings.push(`Migrated config: ${migration.path.join(', ')}`);
312
+ }
313
+ }
314
+ }
315
+ }
316
+
317
+ // Interpolate environment variables
318
+ if (opts.interpolateEnv) {
319
+ processedConfig = processConfigValues(processedConfig);
320
+ }
321
+
322
+ return {
323
+ success: true,
324
+ config: processedConfig,
325
+ warnings: warnings.length > 0 ? warnings : undefined,
326
+ };
327
+ } catch (err) {
328
+ const error = `Config processing failed: ${err.message}`;
329
+ if (opts.throwOnError) {
330
+ throw err;
331
+ }
332
+ return { success: false, error };
333
+ }
334
+ }
335
+
336
+ /**
337
+ * Extract environment variable requirements from config
338
+ * Useful for documentation and validation
339
+ *
340
+ * @param {*} config - Config value
341
+ * @param {Set} found - Set to collect found variables
342
+ * @returns {Array} Array of { name, hasDefault, defaultValue } objects
343
+ */
344
+ export function extractEnvRequirements(config, found = new Set()) {
345
+ const requirements = [];
346
+
347
+ function extract(value) {
348
+ if (typeof value === 'string') {
349
+ const pattern = /\$\{([^}]+)\}/g;
350
+ let match;
351
+ while ((match = pattern.exec(value)) !== null) {
352
+ const content = match[1];
353
+ const colonIndex = content.indexOf(':-');
354
+
355
+ if (colonIndex !== -1) {
356
+ const varName = content.substring(0, colonIndex);
357
+ const defaultValue = content.substring(colonIndex + 2);
358
+ if (!found.has(varName)) {
359
+ found.add(varName);
360
+ requirements.push({ name: varName, hasDefault: true, defaultValue });
361
+ }
362
+ } else {
363
+ if (!found.has(content)) {
364
+ found.add(content);
365
+ requirements.push({ name: content, hasDefault: false });
366
+ }
367
+ }
368
+ }
369
+ } else if (Array.isArray(value)) {
370
+ value.forEach(extract);
371
+ } else if (value && typeof value === 'object' && value.constructor === Object) {
372
+ Object.values(value).forEach(extract);
373
+ }
374
+ }
375
+
376
+ extract(config);
377
+ return requirements;
378
+ }
379
+
380
+ /**
381
+ * Create a config preprocessor for widget loader integration
382
+ * @param {Object} options - Processing options
383
+ * @returns {Function} Preprocessor function
384
+ */
385
+ export function createConfigPreprocessor(options = {}) {
386
+ return (config) => processWidgetConfig(config, options);
387
+ }
388
+
389
+ export default {
390
+ interpolateEnvVars,
391
+ processConfigValues,
392
+ processWidgetConfig,
393
+ validateConfigVersion,
394
+ migrateConfig,
395
+ registerMigration,
396
+ compareVersions,
397
+ extractEnvRequirements,
398
+ createConfigPreprocessor,
399
+ CONFIG_VERSION,
400
+ DEFAULT_PROCESSING_OPTIONS,
401
+ };