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,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
|
+
};
|