claw-dashboard 1.8.4 → 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 +5331 -512
  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,581 @@
1
+ /**
2
+ * Export Scheduler Module
3
+ * Provides cron-style scheduled auto-export of metrics to CSV/JSON
4
+ * Allows users to configure recurring exports for historical tracking
5
+ */
6
+
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ import logger from './logger.js';
11
+ import { PATHS, DASHBOARD_VERSION } from './config.js';
12
+ import { createSnapshot, exportSnapshotToFile, getSnapshotsDirectory } from './snapshot.js';
13
+ import { validateType } from './validation.js';
14
+
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = path.dirname(__filename);
17
+
18
+ /**
19
+ * Default export schedule configuration
20
+ */
21
+ export const DEFAULT_SCHEDULE_CONFIG = {
22
+ enabled: false,
23
+ format: 'json', // 'json' or 'csv'
24
+ directory: null, // null = use default snapshots directory
25
+ filename: null, // null = auto-generated with timestamp
26
+ schedule: '0 * * * *', // cron expression: every hour at minute 0
27
+ includeMetrics: true, // include current metrics in export
28
+ compressOlder: false, // compress exports older than 24h
29
+ retentionDays: 30, // keep exports for 30 days (0 = forever)
30
+ };
31
+
32
+ /**
33
+ * Cron expression parser - supports basic 5-field expressions
34
+ * Fields: minute hour dayOfMonth month dayOfWeek
35
+ */
36
+ class CronParser {
37
+ /**
38
+ * Parse a cron expression into field constraints
39
+ * @param {string} expression - Cron expression (5 fields)
40
+ * @returns {Object} Parsed cron fields
41
+ */
42
+ static parse(expression) {
43
+ const parts = expression.trim().split(/\s+/);
44
+
45
+ if (parts.length !== 5) {
46
+ throw new Error(`Invalid cron expression: expected 5 fields, got ${parts.length}`);
47
+ }
48
+
49
+ const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
50
+
51
+ return {
52
+ minute: this._parseField(minute, 0, 59),
53
+ hour: this._parseField(hour, 0, 23),
54
+ dayOfMonth: this._parseField(dayOfMonth, 1, 31),
55
+ month: this._parseField(month, 1, 12),
56
+ dayOfWeek: this._parseField(dayOfWeek, 0, 6),
57
+ };
58
+ }
59
+
60
+ /**
61
+ * Parse a single cron field into allowed values
62
+ * Supports: *, ranges (1-5), lists (1,3,5), steps (star/5, 1-10/2)
63
+ * @param {string} field - Field value
64
+ * @param {number} min - Minimum value
65
+ * @param {number} max - Maximum value
66
+ * @returns {Set} Set of allowed values
67
+ */
68
+ static _parseField(field, min, max) {
69
+ const values = new Set();
70
+
71
+ // Handle comma-separated parts
72
+ const parts = field.split(',');
73
+
74
+ for (const part of parts) {
75
+ // Handle step values (e.g., every N or range/N)
76
+ const [range, stepStr] = part.split('/');
77
+ const step = stepStr ? parseInt(stepStr, 10) : 1;
78
+
79
+ if (range === '*') {
80
+ // All values with step
81
+ for (let i = min; i <= max; i += step) {
82
+ values.add(i);
83
+ }
84
+ } else if (range.includes('-')) {
85
+ // Range (e.g., 1-5)
86
+ const [startStr, endStr] = range.split('-');
87
+ const start = parseInt(startStr, 10);
88
+ const end = parseInt(endStr, 10);
89
+
90
+ if (isNaN(start) || isNaN(end) || start < min || end > max || start > end) {
91
+ throw new Error(`Invalid range: ${range}`);
92
+ }
93
+
94
+ for (let i = start; i <= end; i += step) {
95
+ values.add(i);
96
+ }
97
+ } else {
98
+ // Single value
99
+ const value = parseInt(range, 10);
100
+ if (isNaN(value) || value < min || value > max) {
101
+ throw new Error(`Invalid value: ${range}`);
102
+ }
103
+ values.add(value);
104
+ }
105
+ }
106
+
107
+ return values;
108
+ }
109
+
110
+ /**
111
+ * Check if a given time matches the cron expression
112
+ * @param {Date} date - Date to check
113
+ * @param {Object} parsed - Parsed cron fields
114
+ * @returns {boolean} True if time matches
115
+ */
116
+ static matches(date, parsed) {
117
+ const minute = date.getMinutes();
118
+ const hour = date.getHours();
119
+ const day = date.getDate();
120
+ const month = date.getMonth() + 1; // JS months are 0-indexed
121
+ const dayOfWeek = date.getDay();
122
+
123
+ return (
124
+ parsed.minute.has(minute) &&
125
+ parsed.hour.has(hour) &&
126
+ parsed.dayOfMonth.has(day) &&
127
+ parsed.month.has(month) &&
128
+ parsed.dayOfWeek.has(dayOfWeek)
129
+ );
130
+ }
131
+
132
+ /**
133
+ * Calculate the next execution time from a given date
134
+ * @param {Date} fromDate - Starting date
135
+ * @param {string} expression - Cron expression
136
+ * @returns {Date} Next execution date
137
+ */
138
+ static nextExecution(fromDate, expression) {
139
+ const parsed = this.parse(expression);
140
+ const date = new Date(fromDate);
141
+
142
+ // Start from next minute
143
+ date.setSeconds(0, 0);
144
+ date.setMinutes(date.getMinutes() + 1);
145
+
146
+ // Search for next matching time (max 1 year ahead)
147
+ const maxIterations = 366 * 24 * 60;
148
+ for (let i = 0; i < maxIterations; i++) {
149
+ if (this.matches(date, parsed)) {
150
+ return date;
151
+ }
152
+ date.setMinutes(date.getMinutes() + 1);
153
+ }
154
+
155
+ throw new Error('Could not find next execution time within 1 year');
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Export Scheduler - manages scheduled metric exports
161
+ */
162
+ export class ExportScheduler {
163
+ constructor(options = {}) {
164
+ this.config = { ...DEFAULT_SCHEDULE_CONFIG, ...options };
165
+ this.enabled = this.config.enabled;
166
+ this.timer = null;
167
+ this.lastExport = null;
168
+ this.nextExport = null;
169
+ this.exportCount = 0;
170
+ this.failedCount = 0;
171
+
172
+ // Metrics data callback (set by dashboard)
173
+ this.getMetricsCallback = null;
174
+
175
+ // Export directory
176
+ this.exportDir = this.config.directory || getSnapshotsDirectory();
177
+ }
178
+
179
+ /**
180
+ * Set the metrics callback function
181
+ * @param {Function} callback - Function that returns current metrics
182
+ */
183
+ setMetricsCallback(callback) {
184
+ if (typeof callback !== 'function') {
185
+ throw new Error('Metrics callback must be a function');
186
+ }
187
+ this.getMetricsCallback = callback;
188
+ }
189
+
190
+ /**
191
+ * Update scheduler configuration
192
+ * @param {Object} newConfig - New configuration values
193
+ */
194
+ configure(newConfig) {
195
+ const validatedConfig = ExportScheduler.validateConfig(newConfig);
196
+
197
+ this.config = { ...this.config, ...validatedConfig };
198
+ this.enabled = this.config.enabled;
199
+ this.exportDir = this.config.directory || getSnapshotsDirectory();
200
+
201
+ // Restart timer if enabled
202
+ if (this.enabled && this.timer) {
203
+ this.stop();
204
+ this.start();
205
+ }
206
+
207
+ logger.info(`Export scheduler configured: ${this.enabled ? 'enabled' : 'disabled'}`);
208
+ }
209
+
210
+ /**
211
+ * Start the scheduler
212
+ */
213
+ start() {
214
+ if (!this.enabled) {
215
+ logger.debug('Export scheduler not enabled, skipping start');
216
+ return;
217
+ }
218
+
219
+ try {
220
+ // Calculate next export time
221
+ this.nextExport = CronParser.nextExecution(new Date(), this.config.schedule);
222
+
223
+ const delay = this.nextExport.getTime() - Date.now();
224
+ logger.info(`Export scheduler started, next export in ${this._formatDelay(delay)}`);
225
+
226
+ // Set timer for next export
227
+ this.timer = setTimeout(() => this._onExportTime(), delay);
228
+ } catch (err) {
229
+ logger.error(`Failed to start export scheduler: ${err.message}`);
230
+ this.enabled = false;
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Stop the scheduler
236
+ */
237
+ stop() {
238
+ if (this.timer) {
239
+ clearTimeout(this.timer);
240
+ this.timer = null;
241
+ logger.debug('Export scheduler stopped');
242
+ }
243
+ }
244
+
245
+ /**
246
+ * Trigger an immediate export (manual)
247
+ * @returns {Promise<Object>} Export result
248
+ */
249
+ async triggerExport() {
250
+ return this._performExport('manual');
251
+ }
252
+
253
+ /**
254
+ * Handle scheduled export time
255
+ * @private
256
+ */
257
+ async _onExportTime() {
258
+ try {
259
+ const result = await this._performExport('scheduled');
260
+
261
+ if (result.success) {
262
+ this.lastExport = new Date();
263
+ this.exportCount++;
264
+ logger.info(`Scheduled export completed: ${result.path}`);
265
+ } else {
266
+ this.failedCount++;
267
+ logger.error(`Scheduled export failed: ${result.error}`);
268
+ }
269
+ } catch (err) {
270
+ this.failedCount++;
271
+ logger.error(`Export scheduler error: ${err.message}`);
272
+ }
273
+
274
+ // Schedule next export
275
+ if (this.enabled) {
276
+ this.nextExport = CronParser.nextExecution(new Date(), this.config.schedule);
277
+ const delay = this.nextExport.getTime() - Date.now();
278
+ this.timer = setTimeout(() => this._onExportTime(), delay);
279
+ logger.debug(`Next export scheduled for ${this.nextExport.toISOString()}`);
280
+ }
281
+ }
282
+
283
+ /**
284
+ * Perform the actual export
285
+ * @private
286
+ * @param {string} trigger - 'manual' or 'scheduled'
287
+ * @returns {Promise<Object>} Export result
288
+ */
289
+ async _performExport(trigger) {
290
+ try {
291
+ // Ensure export directory exists
292
+ if (!fs.existsSync(this.exportDir)) {
293
+ fs.mkdirSync(this.exportDir, { recursive: true });
294
+ }
295
+
296
+ // Generate filename
297
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
298
+ const filename = this.config.filename
299
+ ? `${this.config.filename}-${timestamp}.${this.config.format}`
300
+ : `claw-export-${timestamp}.${this.config.format}`;
301
+
302
+ const filePath = path.join(this.exportDir, filename);
303
+
304
+ // Get data to export
305
+ const exportData = await this._getExportData(trigger);
306
+
307
+ // Write to file
308
+ const content = this.config.format === 'csv'
309
+ ? this._convertToCSV(exportData)
310
+ : JSON.stringify(exportData, null, 2);
311
+
312
+ fs.writeFileSync(filePath, content);
313
+
314
+ // Set secure permissions
315
+ try {
316
+ fs.chmodSync(filePath, 0o600);
317
+ } catch (permErr) {
318
+ logger.warn(`Could not set permissions on export: ${permErr.message}`);
319
+ }
320
+
321
+ // Cleanup old exports if retention is configured
322
+ if (this.config.retentionDays > 0) {
323
+ this._cleanupOldExports();
324
+ }
325
+
326
+ return { success: true, path: filePath, data: exportData };
327
+ } catch (err) {
328
+ return { success: false, error: err.message };
329
+ }
330
+ }
331
+
332
+ /**
333
+ * Get data for export
334
+ * @private
335
+ * @param {string} trigger - Export trigger type
336
+ * @returns {Promise<Object>} Export data
337
+ */
338
+ async _getExportData(trigger) {
339
+ const data = {
340
+ schemaVersion: '1.0.0',
341
+ dashboardVersion: DASHBOARD_VERSION,
342
+ exportedAt: new Date().toISOString(),
343
+ trigger,
344
+ format: this.config.format,
345
+ schedule: this.config.schedule,
346
+ metrics: null,
347
+ };
348
+
349
+ // Include current metrics if callback is set
350
+ if (this.config.includeMetrics && typeof this.getMetricsCallback === 'function') {
351
+ try {
352
+ data.metrics = await this.getMetricsCallback();
353
+ } catch (err) {
354
+ logger.warn(`Failed to collect metrics for export: ${err.message}`);
355
+ data.metrics = { error: 'Failed to collect metrics' };
356
+ }
357
+ }
358
+
359
+ return data;
360
+ }
361
+
362
+ /**
363
+ * Convert export data to CSV format
364
+ * @private
365
+ * @param {Object} data - Export data
366
+ * @returns {string} CSV string
367
+ */
368
+ _convertToCSV(data) {
369
+ const lines = [];
370
+
371
+ // Header comment
372
+ lines.push(`# Claw Dashboard Export - ${data.exportedAt}`);
373
+ lines.push(`# Format: ${data.format}`);
374
+ lines.push(`# Schedule: ${data.schedule}`);
375
+ lines.push('');
376
+
377
+ if (data.metrics) {
378
+ // Flatten metrics into CSV rows
379
+ const metrics = data.metrics;
380
+ const timestamp = data.exportedAt;
381
+
382
+ // Create header row
383
+ const headers = ['timestamp'];
384
+ const values = [timestamp];
385
+
386
+ // Extract key metrics
387
+ if (metrics.cpu !== undefined) {
388
+ headers.push('cpu_percent');
389
+ values.push(metrics.cpu);
390
+ }
391
+ if (metrics.memory !== undefined) {
392
+ headers.push('memory_percent');
393
+ values.push(metrics.memory);
394
+ }
395
+ if (metrics.disk !== undefined) {
396
+ headers.push('disk_percent');
397
+ values.push(metrics.disk);
398
+ }
399
+ if (metrics.network !== undefined) {
400
+ if (metrics.network.rx !== undefined) {
401
+ headers.push('network_rx_bytes');
402
+ values.push(metrics.network.rx);
403
+ }
404
+ if (metrics.network.tx !== undefined) {
405
+ headers.push('network_tx_bytes');
406
+ values.push(metrics.network.tx);
407
+ }
408
+ }
409
+
410
+ lines.push(headers.join(','));
411
+ lines.push(values.join(','));
412
+ }
413
+
414
+ return lines.join('\n');
415
+ }
416
+
417
+ /**
418
+ * Cleanup old exports based on retention policy
419
+ * @private
420
+ */
421
+ _cleanupOldExports() {
422
+ try {
423
+ const cutoff = Date.now() - (this.config.retentionDays * 24 * 60 * 60 * 1000);
424
+ const files = fs.readdirSync(this.exportDir)
425
+ .filter(f => f.startsWith('claw-export-'))
426
+ .map(f => ({
427
+ name: f,
428
+ path: path.join(this.exportDir, f),
429
+ mtime: fs.statSync(path.join(this.exportDir, f)).mtimeMs
430
+ }));
431
+
432
+ for (const file of files) {
433
+ if (file.mtime < cutoff) {
434
+ fs.unlinkSync(file.path);
435
+ logger.debug(`Cleaned up old export: ${file.name}`);
436
+ }
437
+ }
438
+ } catch (err) {
439
+ logger.warn(`Failed to cleanup old exports: ${err.message}`);
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Format delay in human-readable form
445
+ * @private
446
+ * @param {number} ms - Delay in milliseconds
447
+ * @returns {string} Formatted delay
448
+ */
449
+ _formatDelay(ms) {
450
+ const minutes = Math.floor(ms / 60000);
451
+ const hours = Math.floor(minutes / 60);
452
+ const days = Math.floor(hours / 24);
453
+
454
+ if (days > 0) {
455
+ return `${days}d ${hours % 24}h`;
456
+ }
457
+ if (hours > 0) {
458
+ return `${hours}h ${minutes % 60}m`;
459
+ }
460
+ return `${minutes}m`;
461
+ }
462
+
463
+ /**
464
+ * Get scheduler status
465
+ * @returns {Object} Status information
466
+ */
467
+ getStatus() {
468
+ return {
469
+ enabled: this.enabled,
470
+ schedule: this.config.schedule,
471
+ format: this.config.format,
472
+ exportDir: this.exportDir,
473
+ lastExport: this.lastExport?.toISOString(),
474
+ nextExport: this.nextExport?.toISOString(),
475
+ exportCount: this.exportCount,
476
+ failedCount: this.failedCount,
477
+ retentionDays: this.config.retentionDays,
478
+ };
479
+ }
480
+
481
+ /**
482
+ * Validate export scheduler configuration
483
+ * @static
484
+ * @param {Object} config - Configuration to validate
485
+ * @returns {Object} Validated configuration
486
+ */
487
+ static validateConfig(config) {
488
+ const validated = {};
489
+ const errors = [];
490
+
491
+ // Validate enabled
492
+ if (config.enabled !== undefined) {
493
+ validated.enabled = Boolean(config.enabled);
494
+ }
495
+
496
+ // Validate format
497
+ if (config.format !== undefined) {
498
+ if (['json', 'csv'].includes(config.format)) {
499
+ validated.format = config.format;
500
+ } else {
501
+ errors.push(`Invalid format: ${config.format}`);
502
+ }
503
+ }
504
+
505
+ // Validate schedule (cron expression)
506
+ if (config.schedule !== undefined) {
507
+ try {
508
+ CronParser.parse(config.schedule);
509
+ validated.schedule = config.schedule;
510
+ } catch (err) {
511
+ errors.push(`Invalid cron expression: ${config.schedule}`);
512
+ }
513
+ }
514
+
515
+ // Validate directory
516
+ if (config.directory !== undefined) {
517
+ if (config.directory === null || typeof config.directory === 'string') {
518
+ validated.directory = config.directory;
519
+ } else {
520
+ errors.push('Directory must be a string or null');
521
+ }
522
+ }
523
+
524
+ // Validate retention days
525
+ if (config.retentionDays !== undefined) {
526
+ const days = Number(config.retentionDays);
527
+ if (!isNaN(days) && days >= 0 && days <= 365) {
528
+ validated.retentionDays = days;
529
+ } else {
530
+ errors.push('retentionDays must be 0-365');
531
+ }
532
+ }
533
+
534
+ // Validate includeMetrics
535
+ if (config.includeMetrics !== undefined) {
536
+ validated.includeMetrics = Boolean(config.includeMetrics);
537
+ }
538
+
539
+ if (errors.length > 0) {
540
+ logger.warn(`Export scheduler config validation warnings: ${errors.join('; ')}`);
541
+ }
542
+
543
+ return validated;
544
+ }
545
+ }
546
+
547
+ /**
548
+ * Common cron expressions for convenience
549
+ */
550
+ export const CRON_PRESETS = {
551
+ everyMinute: '* * * * *',
552
+ every5Minutes: '*/5 * * * *',
553
+ every10Minutes: '*/10 * * * *',
554
+ every15Minutes: '*/15 * * * *',
555
+ every30Minutes: '*/30 * * * *',
556
+ hourly: '0 * * * *',
557
+ every6Hours: '0 */6 * * *',
558
+ every12Hours: '0 */12 * * *',
559
+ daily: '0 0 * * *',
560
+ weekly: '0 0 * * 0',
561
+ monthly: '0 0 1 * *',
562
+ };
563
+
564
+ /**
565
+ * Create a scheduler instance with default settings
566
+ * @param {Object} options - Scheduler options
567
+ * @returns {ExportScheduler} Scheduler instance
568
+ */
569
+ export function createScheduler(options = {}) {
570
+ return new ExportScheduler(options);
571
+ }
572
+
573
+ export { CronParser };
574
+
575
+ export default {
576
+ ExportScheduler,
577
+ CronParser,
578
+ createScheduler,
579
+ DEFAULT_SCHEDULE_CONFIG,
580
+ CRON_PRESETS,
581
+ };