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,411 @@
1
+ /**
2
+ * Theme Selector UI Module
3
+ * Provides an interactive theme selection interface with live preview
4
+ */
5
+
6
+ import logger from './logger.js';
7
+ import {
8
+ getThemeNames,
9
+ getTheme,
10
+ getCurrentTheme,
11
+ getThemeName,
12
+ setTheme,
13
+ saveTheme,
14
+ onThemeChange
15
+ } from './themes.js';
16
+
17
+ /**
18
+ * Theme preview card dimensions and layout
19
+ */
20
+ const PREVIEW_CARD = {
21
+ width: 28,
22
+ height: 14,
23
+ margin: 2
24
+ };
25
+
26
+ /**
27
+ * Sample content for theme preview
28
+ * Shows various UI elements that demonstrate the theme
29
+ */
30
+ const PREVIEW_SAMPLES = {
31
+ header: 'Theme Preview',
32
+ border: '─'.repeat(24),
33
+ textSample: 'Aa Bb Cc 123',
34
+ statusActive: '● Active',
35
+ statusIdle: '○ Idle',
36
+ gauge: '████░░░░░░ 40%',
37
+ chart: '▁▂▃▄▅▆▇█'
38
+ };
39
+
40
+ /**
41
+ * Create theme selector modal
42
+ * @param {blessed.Screen} screen - Blessed screen instance
43
+ * @param {Object} blessed - Blessed library
44
+ * @param {Function} onClose - Callback when modal closes
45
+ * @returns {Object} Theme selector controller
46
+ */
47
+ export function createThemeSelector(screen, blessed, onClose) {
48
+ const themeNames = getThemeNames();
49
+ const currentThemeName = getThemeName();
50
+ let selectedIndex = themeNames.indexOf(currentThemeName);
51
+ if (selectedIndex === -1) selectedIndex = 0;
52
+
53
+ let modalBox = null;
54
+ let previewBoxes = [];
55
+ let infoText = null;
56
+ let helpText = null;
57
+ let unsubscribeThemeChange = null;
58
+
59
+ /**
60
+ * Apply color to blessed element from theme
61
+ */
62
+ function applyThemeColor(element, color, isBg = false) {
63
+ if (isBg) {
64
+ element.style.bg = color;
65
+ } else {
66
+ element.style.fg = color;
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Render theme preview content with colors
72
+ */
73
+ function renderPreviewContent(theme, blessed) {
74
+ const colors = theme.colors;
75
+ return [
76
+ `{${colors.branding.title}-fg}{bold}${PREVIEW_SAMPLES.header}{/bold}{/${colors.branding.title}-fg}`,
77
+ `{${colors.border.cpu}-fg}${PREVIEW_SAMPLES.border}{/${colors.border.cpu}-fg}`,
78
+ '',
79
+ ` {${colors.text.primary}-fg}${PREVIEW_SAMPLES.textSample}{/${colors.text.primary}-fg}`,
80
+ '',
81
+ ` {${colors.status.active}-fg}${PREVIEW_SAMPLES.statusActive}{/${colors.status.active}-fg}`,
82
+ ` {${colors.status.idle}-fg}${PREVIEW_SAMPLES.statusIdle}{/${colors.status.idle}-fg}`,
83
+ '',
84
+ ` {${colors.gauge.medium}-fg}${PREVIEW_SAMPLES.gauge}{/${colors.gauge.medium}-fg}`,
85
+ '',
86
+ ` {${colors.chart.line}-fg}${PREVIEW_SAMPLES.chart}{/${colors.chart.line}-fg}`,
87
+ '',
88
+ `{center}{${colors.text.secondary}-fg}${theme.name}{/${colors.text.secondary}-fg}{/center}`
89
+ ].join('\n');
90
+ }
91
+
92
+ /**
93
+ * Create the theme selector modal
94
+ */
95
+ function createModal() {
96
+ const screenWidth = screen.width;
97
+ const screenHeight = screen.height;
98
+
99
+ // Calculate layout
100
+ const cardsPerRow = Math.min(3, themeNames.length);
101
+ const modalWidth = (PREVIEW_CARD.width + PREVIEW_CARD.margin) * cardsPerRow + 4;
102
+ const rows = Math.ceil(themeNames.length / cardsPerRow);
103
+ const modalHeight = PREVIEW_CARD.height * rows + 8;
104
+
105
+ // Main modal container
106
+ modalBox = blessed.box({
107
+ parent: screen,
108
+ top: 'center',
109
+ left: 'center',
110
+ width: Math.min(modalWidth, screenWidth - 4),
111
+ height: Math.min(modalHeight, screenHeight - 4),
112
+ border: { type: 'line' },
113
+ style: {
114
+ border: { fg: 'cyan' },
115
+ bg: 'black'
116
+ },
117
+ tags: true,
118
+ label: ' {bold}Theme Selector{/bold} ',
119
+ scrollable: true,
120
+ alwaysScroll: true,
121
+ scrollbar: {
122
+ ch: ' ',
123
+ style: { bg: 'cyan' }
124
+ }
125
+ });
126
+
127
+ // Title
128
+ blessed.text({
129
+ parent: modalBox,
130
+ top: 1,
131
+ left: 'center',
132
+ width: modalWidth - 4,
133
+ content: '{center}Select a theme with arrow keys, press Enter to apply{/center}',
134
+ style: { fg: 'white' },
135
+ tags: true
136
+ });
137
+
138
+ // Create preview cards for each theme
139
+ themeNames.forEach((themeName, index) => {
140
+ const row = Math.floor(index / cardsPerRow);
141
+ const col = index % cardsPerRow;
142
+ const theme = themeName === 'auto' ? getCurrentTheme() : getTheme(themeName);
143
+ const isSelected = index === selectedIndex;
144
+ const isCurrent = themeName === currentThemeName;
145
+
146
+ const left = 2 + col * (PREVIEW_CARD.width + PREVIEW_CARD.margin);
147
+ const top = 3 + row * PREVIEW_CARD.height;
148
+
149
+ // Card container
150
+ const card = blessed.box({
151
+ parent: modalBox,
152
+ top,
153
+ left,
154
+ width: PREVIEW_CARD.width,
155
+ height: PREVIEW_CARD.height,
156
+ border: {
157
+ type: 'line',
158
+ fg: isSelected ? 'brightCyan' : 'gray'
159
+ },
160
+ style: {
161
+ bg: isSelected ? 'brightBlack' : 'black',
162
+ border: {
163
+ fg: isSelected ? 'brightCyan' : 'gray'
164
+ }
165
+ },
166
+ tags: true,
167
+ content: renderPreviewContent(theme, blessed)
168
+ });
169
+
170
+ // Selection indicator
171
+ if (isCurrent) {
172
+ blessed.text({
173
+ parent: card,
174
+ top: 0,
175
+ right: 0,
176
+ content: '●',
177
+ style: { fg: 'green' }
178
+ });
179
+ }
180
+
181
+ previewBoxes.push({ box: card, themeName, index });
182
+ });
183
+
184
+ // Info text
185
+ infoText = blessed.text({
186
+ parent: modalBox,
187
+ bottom: 2,
188
+ left: 'center',
189
+ width: modalWidth - 4,
190
+ content: getInfoText(),
191
+ style: { fg: 'gray' },
192
+ tags: true
193
+ });
194
+
195
+ // Help text
196
+ helpText = blessed.text({
197
+ parent: modalBox,
198
+ bottom: 1,
199
+ left: 'center',
200
+ width: modalWidth - 4,
201
+ content: '{center}↑/↓/←/→: Navigate Enter: Apply t: Cycle themes q/Esc: Close{/center}',
202
+ style: { fg: 'gray' },
203
+ tags: true
204
+ });
205
+
206
+ screen.render();
207
+ }
208
+
209
+ /**
210
+ * Get info text for current selection
211
+ */
212
+ function getInfoText() {
213
+ const themeName = themeNames[selectedIndex];
214
+ const theme = getTheme(themeName);
215
+ if (themeName === 'auto') {
216
+ const detected = getCurrentTheme();
217
+ return `{center}Auto-detect → ${detected.name} (currently selected: ${currentThemeName}){/center}`;
218
+ }
219
+ return `{center}${theme.name}${themeName === currentThemeName ? ' (current)' : ''}{/center}`;
220
+ }
221
+
222
+ /**
223
+ * Update selection highlighting
224
+ */
225
+ function updateSelection() {
226
+ previewBoxes.forEach(({ box, index }) => {
227
+ const isSelected = index === selectedIndex;
228
+ const isCurrent = themeNames[index] === currentThemeName;
229
+
230
+ box.style.bg = isSelected ? 'brightBlack' : 'black';
231
+ box.style.border.fg = isSelected ? 'brightCyan' : 'gray';
232
+ });
233
+
234
+ if (infoText) {
235
+ infoText.setContent(getInfoText());
236
+ }
237
+
238
+ screen.render();
239
+ }
240
+
241
+ /**
242
+ * Navigate selection
243
+ */
244
+ function navigate(delta) {
245
+ const cardsPerRow = Math.min(3, themeNames.length);
246
+ const rows = Math.ceil(themeNames.length / cardsPerRow);
247
+
248
+ if (delta === -cardsPerRow && selectedIndex - cardsPerRow >= 0) {
249
+ // Move up
250
+ selectedIndex -= cardsPerRow;
251
+ } else if (delta === cardsPerRow && selectedIndex + cardsPerRow < themeNames.length) {
252
+ // Move down
253
+ selectedIndex += cardsPerRow;
254
+ } else if (delta === -1 && selectedIndex % cardsPerRow > 0) {
255
+ // Move left
256
+ selectedIndex--;
257
+ } else if (delta === 1 && (selectedIndex + 1) % cardsPerRow !== 0 && selectedIndex + 1 < themeNames.length) {
258
+ // Move right
259
+ selectedIndex++;
260
+ }
261
+
262
+ updateSelection();
263
+ }
264
+
265
+ /**
266
+ * Apply selected theme
267
+ */
268
+ function applySelectedTheme() {
269
+ const themeName = themeNames[selectedIndex];
270
+ if (themeName !== currentThemeName) {
271
+ setTheme(themeName);
272
+ saveTheme();
273
+ logger.info(`Theme changed to: ${themeName}`);
274
+ }
275
+ close();
276
+ }
277
+
278
+ /**
279
+ * Cycle to next theme (quick switch)
280
+ */
281
+ function cycleThemeQuick() {
282
+ selectedIndex = (selectedIndex + 1) % themeNames.length;
283
+ const themeName = themeNames[selectedIndex];
284
+ setTheme(themeName);
285
+ saveTheme();
286
+ updateSelection();
287
+
288
+ // Update all preview cards to show current selection
289
+ previewBoxes.forEach(({ box, index }) => {
290
+ const isCurrent = themeNames[index] === themeName;
291
+ // Re-render with updated selection
292
+ const theme = themeNames[index] === 'auto' ? getCurrentTheme() : getTheme(themeNames[index]);
293
+ box.setContent(renderPreviewContent(theme, blessed));
294
+ });
295
+
296
+ screen.render();
297
+ }
298
+
299
+ /**
300
+ * Close the modal
301
+ */
302
+ function close() {
303
+ if (unsubscribeThemeChange) {
304
+ unsubscribeThemeChange();
305
+ }
306
+
307
+ if (modalBox) {
308
+ modalBox.destroy();
309
+ modalBox = null;
310
+ }
311
+
312
+ previewBoxes = [];
313
+
314
+ if (onClose) {
315
+ onClose();
316
+ }
317
+
318
+ screen.render();
319
+ }
320
+
321
+ /**
322
+ * Handle key events
323
+ */
324
+ function handleKey(ch, key) {
325
+ // Navigation
326
+ if (key.name === 'up') navigate(-3);
327
+ else if (key.name === 'down') navigate(3);
328
+ else if (key.name === 'left') navigate(-1);
329
+ else if (key.name === 'right') navigate(1);
330
+ else if (key.name === 'return') applySelectedTheme();
331
+ else if (ch === 't') cycleThemeQuick();
332
+ else if (ch === 'q' || key.name === 'escape') close();
333
+ }
334
+
335
+ // Create the modal
336
+ createModal();
337
+
338
+ // Listen for theme changes from elsewhere
339
+ unsubscribeThemeChange = onThemeChange(() => {
340
+ // Refresh previews if theme changes externally
341
+ previewBoxes.forEach(({ box, index }) => {
342
+ const theme = themeNames[index] === 'auto' ? getCurrentTheme() : getTheme(themeNames[index]);
343
+ box.setContent(renderPreviewContent(theme, blessed));
344
+ });
345
+ screen.render();
346
+ });
347
+
348
+ return {
349
+ handleKey,
350
+ close,
351
+ isActive: () => modalBox !== null
352
+ };
353
+ }
354
+
355
+ /**
356
+ * Show theme selector as a modal overlay
357
+ * @param {blessed.Screen} screen - Blessed screen instance
358
+ * @param {Object} blessed - Blessed library
359
+ * @param {Function} onThemeApplied - Callback when theme is applied
360
+ * @returns {Promise<void>}
361
+ */
362
+ export async function showThemeSelector(screen, blessed, onThemeApplied) {
363
+ return new Promise((resolve) => {
364
+ const selector = createThemeSelector(screen, blessed, () => {
365
+ screen.removeListener('keypress', keyHandler);
366
+ if (onThemeApplied) {
367
+ onThemeApplied();
368
+ }
369
+ resolve();
370
+ });
371
+
372
+ function keyHandler(ch, key) {
373
+ selector.handleKey(ch, key);
374
+ }
375
+
376
+ screen.on('keypress', keyHandler);
377
+ });
378
+ }
379
+
380
+ /**
381
+ * Get theme info for display
382
+ * @param {string} themeName - Theme name
383
+ * @returns {Object} Theme info object
384
+ */
385
+ export function getThemeInfo(themeName) {
386
+ const theme = getTheme(themeName);
387
+ const current = getThemeName();
388
+
389
+ return {
390
+ name: themeName,
391
+ displayName: theme?.name || themeName,
392
+ isCurrent: themeName === current,
393
+ isAuto: themeName === 'auto',
394
+ colors: theme?.colors ? Object.keys(theme.colors) : []
395
+ };
396
+ }
397
+
398
+ /**
399
+ * Get all themes info
400
+ * @returns {Array} Array of theme info objects
401
+ */
402
+ export function getAllThemesInfo() {
403
+ return getThemeNames().map(name => getThemeInfo(name));
404
+ }
405
+
406
+ export default {
407
+ createThemeSelector,
408
+ showThemeSelector,
409
+ getThemeInfo,
410
+ getAllThemesInfo
411
+ };