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
package/src/snapshot.js
ADDED
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Snapshot Module
|
|
3
|
+
* Provides export/import functionality for dashboard configurations
|
|
4
|
+
* Allows users to share layouts and backup settings
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import fs from 'fs';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
import { join } from 'path';
|
|
10
|
+
import { PATHS, DEFAULT_SETTINGS, DASHBOARD_VERSION } from './config.js';
|
|
11
|
+
import logger from './logger.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Snapshot schema version for compatibility checking
|
|
15
|
+
*/
|
|
16
|
+
export const SNAPSHOT_SCHEMA_VERSION = '1.0.0';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Settings keys to export (for a clean, portable snapshot)
|
|
20
|
+
* Excludes: sessionSearchQuery, firstRun (runtime state)
|
|
21
|
+
*/
|
|
22
|
+
const EXPORTABLE_SETTINGS = [
|
|
23
|
+
'refreshInterval',
|
|
24
|
+
'logLevelFilter',
|
|
25
|
+
'sessionSortMode',
|
|
26
|
+
'showWidget1',
|
|
27
|
+
'showWidget2',
|
|
28
|
+
'showWidget3',
|
|
29
|
+
'showWidget4',
|
|
30
|
+
'showWidget5',
|
|
31
|
+
'showWidget6',
|
|
32
|
+
'showWidget7',
|
|
33
|
+
'showWidget8',
|
|
34
|
+
'showWidget9',
|
|
35
|
+
'showPerformanceMetrics',
|
|
36
|
+
'theme',
|
|
37
|
+
'exportFormat',
|
|
38
|
+
'exportDirectory',
|
|
39
|
+
'favorites',
|
|
40
|
+
'showFavoritesOnly',
|
|
41
|
+
'gatewayEndpoints',
|
|
42
|
+
'activeGatewayEndpoint',
|
|
43
|
+
'webInterface',
|
|
44
|
+
'widgetLoading',
|
|
45
|
+
'plugins',
|
|
46
|
+
'autoRetry',
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a snapshot from current settings
|
|
51
|
+
* @param {Object} currentSettings - Current dashboard settings
|
|
52
|
+
* @param {Object} options - Snapshot options
|
|
53
|
+
* @param {string} [options.name] - Optional snapshot name
|
|
54
|
+
* @param {string} [options.description] - Optional description
|
|
55
|
+
* @returns {Object} Snapshot object
|
|
56
|
+
*/
|
|
57
|
+
export function createSnapshot(currentSettings, options = {}) {
|
|
58
|
+
const snapshot = {
|
|
59
|
+
schemaVersion: SNAPSHOT_SCHEMA_VERSION,
|
|
60
|
+
dashboardVersion: DASHBOARD_VERSION,
|
|
61
|
+
createdAt: new Date().toISOString(),
|
|
62
|
+
name: options.name || 'Dashboard Snapshot',
|
|
63
|
+
description: options.description || '',
|
|
64
|
+
platform: {
|
|
65
|
+
os: os.platform(),
|
|
66
|
+
arch: os.arch(),
|
|
67
|
+
nodeVersion: process.version,
|
|
68
|
+
},
|
|
69
|
+
settings: {},
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
// Extract only exportable settings
|
|
73
|
+
for (const key of EXPORTABLE_SETTINGS) {
|
|
74
|
+
if (key in currentSettings) {
|
|
75
|
+
snapshot.settings[key] = currentSettings[key];
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Add metadata about what's included
|
|
80
|
+
snapshot.metadata = {
|
|
81
|
+
widgetCount: [
|
|
82
|
+
'showWidget1', 'showWidget2', 'showWidget3', 'showWidget4',
|
|
83
|
+
'showWidget5', 'showWidget6', 'showWidget7', 'showWidget8', 'showWidget9',
|
|
84
|
+
].filter(w => snapshot.settings[w] !== false).length,
|
|
85
|
+
pluginCount: Object.keys(snapshot.settings.plugins || {}).length,
|
|
86
|
+
endpointCount: (snapshot.settings.gatewayEndpoints || []).length,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
return snapshot;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Validate a snapshot before importing
|
|
94
|
+
* @param {Object} snapshot - Snapshot to validate
|
|
95
|
+
* @returns {Object} Validation result { valid: boolean, error?: string }
|
|
96
|
+
*/
|
|
97
|
+
export function validateSnapshot(snapshot) {
|
|
98
|
+
if (!snapshot || typeof snapshot !== 'object') {
|
|
99
|
+
return { valid: false, error: 'Invalid snapshot format' };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check schema version (allow same major version)
|
|
103
|
+
const schemaVersion = snapshot.schemaVersion || '0.0.0';
|
|
104
|
+
const [major] = schemaVersion.split('.');
|
|
105
|
+
const [currentMajor] = SNAPSHOT_SCHEMA_VERSION.split('.');
|
|
106
|
+
|
|
107
|
+
if (parseInt(major) > parseInt(currentMajor)) {
|
|
108
|
+
return {
|
|
109
|
+
valid: false,
|
|
110
|
+
error: `Snapshot version ${schemaVersion} is newer than supported (${SNAPSHOT_SCHEMA_VERSION})`
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Validate settings object exists
|
|
115
|
+
if (!snapshot.settings || typeof snapshot.settings !== 'object') {
|
|
116
|
+
return { valid: false, error: 'Missing or invalid settings in snapshot' };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Validate critical settings have correct types
|
|
120
|
+
const validations = [
|
|
121
|
+
{ key: 'refreshInterval', type: 'number', min: 500, max: 60000 },
|
|
122
|
+
{ key: 'theme', type: 'string', allowed: ['auto', 'default', 'dark', 'high-contrast', 'ocean'] },
|
|
123
|
+
{ key: 'logLevelFilter', type: 'string', allowed: ['all', 'debug', 'info', 'warn', 'error'] },
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
for (const v of validations) {
|
|
127
|
+
const value = snapshot.settings[v.key];
|
|
128
|
+
if (value !== undefined) {
|
|
129
|
+
if (v.type && typeof value !== v.type) {
|
|
130
|
+
return { valid: false, error: `Invalid type for ${v.key}: expected ${v.type}` };
|
|
131
|
+
}
|
|
132
|
+
if (v.min !== undefined && value < v.min) {
|
|
133
|
+
return { valid: false, error: `${v.key} must be at least ${v.min}` };
|
|
134
|
+
}
|
|
135
|
+
if (v.max !== undefined && value > v.max) {
|
|
136
|
+
return { valid: false, error: `${v.key} must be at most ${v.max}` };
|
|
137
|
+
}
|
|
138
|
+
if (v.allowed && !v.allowed.includes(value)) {
|
|
139
|
+
return { valid: false, error: `${v.key} must be one of: ${v.allowed.join(', ')}` };
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Validate widget booleans
|
|
145
|
+
for (let i = 1; i <= 9; i++) {
|
|
146
|
+
const key = `showWidget${i}`;
|
|
147
|
+
const value = snapshot.settings[key];
|
|
148
|
+
if (value !== undefined && typeof value !== 'boolean') {
|
|
149
|
+
return { valid: false, error: `${key} must be a boolean` };
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return { valid: true };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Merge snapshot settings with defaults (for safe import)
|
|
158
|
+
* Preserves existing settings not in snapshot, applies snapshot settings
|
|
159
|
+
* @param {Object} existingSettings - Current settings
|
|
160
|
+
* @param {Object} snapshotSettings - Settings from snapshot
|
|
161
|
+
* @returns {Object} Merged settings
|
|
162
|
+
*/
|
|
163
|
+
export function mergeSnapshotSettings(existingSettings, snapshotSettings) {
|
|
164
|
+
const merged = { ...existingSettings };
|
|
165
|
+
|
|
166
|
+
// Apply snapshot settings
|
|
167
|
+
for (const key of EXPORTABLE_SETTINGS) {
|
|
168
|
+
if (key in snapshotSettings) {
|
|
169
|
+
// Deep clone to avoid reference issues
|
|
170
|
+
if (typeof snapshotSettings[key] === 'object' && snapshotSettings[key] !== null) {
|
|
171
|
+
merged[key] = JSON.parse(JSON.stringify(snapshotSettings[key]));
|
|
172
|
+
} else {
|
|
173
|
+
merged[key] = snapshotSettings[key];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return merged;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Export snapshot to file
|
|
183
|
+
* @param {Object} snapshot - Snapshot object
|
|
184
|
+
* @param {string} filePath - Target file path
|
|
185
|
+
* @returns {Object} Result { success: boolean, error?: string, path?: string }
|
|
186
|
+
*/
|
|
187
|
+
export function exportSnapshotToFile(snapshot, filePath) {
|
|
188
|
+
try {
|
|
189
|
+
const dir = filePath.substring(0, filePath.lastIndexOf('/'));
|
|
190
|
+
if (dir && !fs.existsSync(dir)) {
|
|
191
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fs.writeFileSync(filePath, JSON.stringify(snapshot, null, 2));
|
|
195
|
+
|
|
196
|
+
// Set secure permissions (owner read/write only)
|
|
197
|
+
try {
|
|
198
|
+
fs.chmodSync(filePath, 0o600);
|
|
199
|
+
} catch (permErr) {
|
|
200
|
+
logger.warn(`Could not set permissions on snapshot: ${permErr.message}`);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return { success: true, path: filePath };
|
|
204
|
+
} catch (err) {
|
|
205
|
+
return { success: false, error: err.message };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Import snapshot from file
|
|
211
|
+
* @param {string} filePath - Path to snapshot file
|
|
212
|
+
* @returns {Object} Result { success: boolean, error?: string, snapshot?: Object }
|
|
213
|
+
*/
|
|
214
|
+
export function importSnapshotFromFile(filePath) {
|
|
215
|
+
try {
|
|
216
|
+
if (!fs.existsSync(filePath)) {
|
|
217
|
+
return { success: false, error: `File not found: ${filePath}` };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
const data = fs.readFileSync(filePath, 'utf8');
|
|
221
|
+
const snapshot = JSON.parse(data);
|
|
222
|
+
|
|
223
|
+
const validation = validateSnapshot(snapshot);
|
|
224
|
+
if (!validation.valid) {
|
|
225
|
+
return { success: false, error: validation.error };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { success: true, snapshot };
|
|
229
|
+
} catch (err) {
|
|
230
|
+
if (err instanceof SyntaxError) {
|
|
231
|
+
return { success: false, error: 'Invalid JSON format' };
|
|
232
|
+
}
|
|
233
|
+
return { success: false, error: err.message };
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Generate default snapshot filename
|
|
239
|
+
* @param {string} [name] - Optional name to include in filename
|
|
240
|
+
* @returns {string} Generated filename
|
|
241
|
+
*/
|
|
242
|
+
export function generateSnapshotFilename(name) {
|
|
243
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
|
|
244
|
+
const safeName = name ? name.replace(/[^a-zA-Z0-9-_]/g, '_') : 'dashboard';
|
|
245
|
+
return `claw-snapshot-${safeName}-${timestamp}.json`;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Get snapshots directory
|
|
250
|
+
* @returns {string} Path to snapshots directory
|
|
251
|
+
*/
|
|
252
|
+
export function getSnapshotsDirectory() {
|
|
253
|
+
return join(PATHS.OPENCLAW_DIR, 'snapshots');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* List available snapshots
|
|
258
|
+
* @returns {Array} Array of snapshot info objects
|
|
259
|
+
*/
|
|
260
|
+
export function listSnapshots() {
|
|
261
|
+
const dir = getSnapshotsDirectory();
|
|
262
|
+
if (!fs.existsSync(dir)) {
|
|
263
|
+
return [];
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
try {
|
|
267
|
+
const files = fs.readdirSync(dir)
|
|
268
|
+
.filter(f => f.endsWith('.json'))
|
|
269
|
+
.map(f => {
|
|
270
|
+
const path = join(dir, f);
|
|
271
|
+
try {
|
|
272
|
+
const data = fs.readFileSync(path, 'utf8');
|
|
273
|
+
const snapshot = JSON.parse(data);
|
|
274
|
+
const stats = fs.statSync(path);
|
|
275
|
+
return {
|
|
276
|
+
filename: f,
|
|
277
|
+
path,
|
|
278
|
+
name: snapshot.name || 'Unnamed',
|
|
279
|
+
description: snapshot.description || '',
|
|
280
|
+
createdAt: snapshot.createdAt || stats.mtime.toISOString(),
|
|
281
|
+
dashboardVersion: snapshot.dashboardVersion || 'unknown',
|
|
282
|
+
schemaVersion: snapshot.schemaVersion || 'unknown',
|
|
283
|
+
metadata: snapshot.metadata || {},
|
|
284
|
+
};
|
|
285
|
+
} catch (err) {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
.filter(Boolean)
|
|
290
|
+
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
|
291
|
+
|
|
292
|
+
return files;
|
|
293
|
+
} catch (err) {
|
|
294
|
+
logger.warn(`Failed to list snapshots: ${err.message}`);
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Delete a snapshot
|
|
301
|
+
* @param {string} filename - Snapshot filename
|
|
302
|
+
* @returns {Object} Result { success: boolean, error?: string }
|
|
303
|
+
*/
|
|
304
|
+
export function deleteSnapshot(filename) {
|
|
305
|
+
const dir = getSnapshotsDirectory();
|
|
306
|
+
const filePath = join(dir, filename);
|
|
307
|
+
|
|
308
|
+
// Security: ensure the resolved path is within snapshots directory
|
|
309
|
+
const resolvedPath = fs.realpathSync.safe
|
|
310
|
+
? fs.realpathSync(filePath)
|
|
311
|
+
: filePath;
|
|
312
|
+
|
|
313
|
+
if (!resolvedPath.startsWith(dir)) {
|
|
314
|
+
return { success: false, error: 'Invalid snapshot path' };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
try {
|
|
318
|
+
if (!fs.existsSync(filePath)) {
|
|
319
|
+
return { success: false, error: 'Snapshot not found' };
|
|
320
|
+
}
|
|
321
|
+
fs.unlinkSync(filePath);
|
|
322
|
+
return { success: true };
|
|
323
|
+
} catch (err) {
|
|
324
|
+
return { success: false, error: err.message };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Get snapshot summary for display
|
|
330
|
+
* @param {Object} snapshot - Snapshot object
|
|
331
|
+
* @returns {string} Formatted summary
|
|
332
|
+
*/
|
|
333
|
+
export function getSnapshotSummary(snapshot) {
|
|
334
|
+
if (!snapshot) return 'Invalid snapshot';
|
|
335
|
+
|
|
336
|
+
const lines = [
|
|
337
|
+
`Name: ${snapshot.name || 'Unnamed'}`,
|
|
338
|
+
`Created: ${snapshot.createdAt ? new Date(snapshot.createdAt).toLocaleString() : 'Unknown'}`,
|
|
339
|
+
];
|
|
340
|
+
|
|
341
|
+
if (snapshot.description) {
|
|
342
|
+
lines.push(`Description: ${snapshot.description}`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (snapshot.metadata) {
|
|
346
|
+
const { widgetCount, pluginCount, endpointCount } = snapshot.metadata;
|
|
347
|
+
lines.push(`Widgets: ${widgetCount}, Plugins: ${pluginCount}, Endpoints: ${endpointCount}`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
if (snapshot.settings) {
|
|
351
|
+
const theme = snapshot.settings.theme || 'auto';
|
|
352
|
+
const refresh = snapshot.settings.refreshInterval || 2000;
|
|
353
|
+
lines.push(`Theme: ${theme}, Refresh: ${refresh}ms`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return lines.join('\n');
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export default {
|
|
360
|
+
SNAPSHOT_SCHEMA_VERSION,
|
|
361
|
+
EXPORTABLE_SETTINGS,
|
|
362
|
+
createSnapshot,
|
|
363
|
+
validateSnapshot,
|
|
364
|
+
mergeSnapshotSettings,
|
|
365
|
+
exportSnapshotToFile,
|
|
366
|
+
importSnapshotFromFile,
|
|
367
|
+
generateSnapshotFilename,
|
|
368
|
+
getSnapshotsDirectory,
|
|
369
|
+
listSnapshots,
|
|
370
|
+
deleteSnapshot,
|
|
371
|
+
getSnapshotSummary,
|
|
372
|
+
};
|
package/src/splash.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import blessed from 'blessed';
|
|
2
|
+
import { DASHBOARD_VERSION } from './config.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Shows a fullscreen splash screen with loading progress
|
|
6
|
+
* @param {blessed.Screen} screen - The blessed screen
|
|
7
|
+
* @param {Function} loadFn - Async function that performs loading
|
|
8
|
+
* @returns {Promise<void>}
|
|
9
|
+
*/
|
|
10
|
+
export async function showSplashScreen(screen, loadFn) {
|
|
11
|
+
// Create fullscreen overlay
|
|
12
|
+
const splashBox = blessed.box({
|
|
13
|
+
parent: screen,
|
|
14
|
+
top: 0,
|
|
15
|
+
left: 0,
|
|
16
|
+
width: '100%',
|
|
17
|
+
height: '100%',
|
|
18
|
+
style: { bg: 'black' }
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// Simple centered text - no ASCII art, just basic characters
|
|
22
|
+
blessed.text({
|
|
23
|
+
parent: splashBox,
|
|
24
|
+
top: Math.floor(screen.height / 2) - 4,
|
|
25
|
+
left: 'center',
|
|
26
|
+
content: 'CLAW DASHBOARD',
|
|
27
|
+
style: { fg: 'brightCyan', bold: true }
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
blessed.text({
|
|
31
|
+
parent: splashBox,
|
|
32
|
+
top: Math.floor(screen.height / 2) - 2,
|
|
33
|
+
left: 'center',
|
|
34
|
+
content: '================',
|
|
35
|
+
style: { fg: 'cyan' }
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
blessed.text({
|
|
39
|
+
parent: splashBox,
|
|
40
|
+
top: Math.floor(screen.height / 2),
|
|
41
|
+
left: 'center',
|
|
42
|
+
content: `v${DASHBOARD_VERSION}`,
|
|
43
|
+
style: { fg: 'gray' }
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Status line
|
|
47
|
+
const statusText = blessed.text({
|
|
48
|
+
parent: splashBox,
|
|
49
|
+
top: Math.floor(screen.height / 2) + 3,
|
|
50
|
+
left: 'center',
|
|
51
|
+
width: 35,
|
|
52
|
+
height: 1,
|
|
53
|
+
content: 'Initializing...',
|
|
54
|
+
style: { fg: 'white', bold: true },
|
|
55
|
+
align: 'center'
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Spinner next to status
|
|
59
|
+
const spinnerText = blessed.text({
|
|
60
|
+
parent: splashBox,
|
|
61
|
+
top: Math.floor(screen.height / 2) + 3,
|
|
62
|
+
left: Math.floor(screen.width / 2) + 18,
|
|
63
|
+
content: '*',
|
|
64
|
+
style: { fg: 'brightGreen', bold: true }
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Progress bar
|
|
68
|
+
const progressBar = blessed.text({
|
|
69
|
+
parent: splashBox,
|
|
70
|
+
top: Math.floor(screen.height / 2) + 5,
|
|
71
|
+
left: 'center',
|
|
72
|
+
width: 30,
|
|
73
|
+
content: '[----------------------]',
|
|
74
|
+
style: { fg: 'gray' },
|
|
75
|
+
align: 'center'
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
screen.render();
|
|
79
|
+
|
|
80
|
+
// Spinner animation
|
|
81
|
+
const spinnerFrames = ['|', '/', '-', '\\\\'];
|
|
82
|
+
let spinnerIdx = 0;
|
|
83
|
+
const spinnerInterval = setInterval(() => {
|
|
84
|
+
spinnerIdx = (spinnerIdx + 1) % spinnerFrames.length;
|
|
85
|
+
spinnerText.setContent(spinnerFrames[spinnerIdx]);
|
|
86
|
+
screen.render();
|
|
87
|
+
}, 150);
|
|
88
|
+
|
|
89
|
+
// Update progress function
|
|
90
|
+
const updateProgress = (progress, status) => {
|
|
91
|
+
if (status) statusText.setContent(status);
|
|
92
|
+
|
|
93
|
+
const filled = Math.floor((progress / 100) * 22);
|
|
94
|
+
const bar = '[' + '#'.repeat(filled) + '-'.repeat(22 - filled) + ']';
|
|
95
|
+
progressBar.setContent(bar);
|
|
96
|
+
progressBar.style.fg = progress < 50 ? 'red' : progress < 80 ? 'yellow' : 'green';
|
|
97
|
+
|
|
98
|
+
screen.render();
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// Execute loading
|
|
102
|
+
try {
|
|
103
|
+
if (loadFn) {
|
|
104
|
+
await loadFn(updateProgress);
|
|
105
|
+
}
|
|
106
|
+
updateProgress(100, 'Ready!');
|
|
107
|
+
await new Promise(r => setTimeout(r, 400));
|
|
108
|
+
} finally {
|
|
109
|
+
clearInterval(spinnerInterval);
|
|
110
|
+
splashBox.destroy();
|
|
111
|
+
screen.render();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export default { showSplashScreen };
|