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,320 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for Command Palette Lifecycle
|
|
3
|
+
* Verifies the command palette modal behavior and navigation guards
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { jest } from '@jest/globals';
|
|
7
|
+
|
|
8
|
+
describe('Command Palette Lifecycle', () => {
|
|
9
|
+
describe('_commandPaletteClosing flag behavior', () => {
|
|
10
|
+
test('should block navigation when command palette is closing', () => {
|
|
11
|
+
// Simulate the state during command palette close transition
|
|
12
|
+
const state = {
|
|
13
|
+
_commandPaletteClosing: false,
|
|
14
|
+
w: {
|
|
15
|
+
commandPaletteBox: null,
|
|
16
|
+
commandPaletteInput: null,
|
|
17
|
+
commandPaletteList: null,
|
|
18
|
+
},
|
|
19
|
+
isModalActive: false,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Simulate navigation guard logic
|
|
23
|
+
const shouldBlockNavigation = () => {
|
|
24
|
+
return !!(state._commandPaletteClosing || (state.w.commandPaletteBox && state.w.commandPaletteInput && state.w.commandPaletteInput.focused));
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Initially, navigation should NOT be blocked
|
|
28
|
+
expect(shouldBlockNavigation()).toBe(false);
|
|
29
|
+
|
|
30
|
+
// Simulate opening command palette
|
|
31
|
+
state.w.commandPaletteBox = { destroy: jest.fn() };
|
|
32
|
+
state.w.commandPaletteInput = { focused: true };
|
|
33
|
+
state.w.commandPaletteList = { focus: jest.fn() };
|
|
34
|
+
state.isModalActive = true;
|
|
35
|
+
expect(shouldBlockNavigation()).toBe(true); // Blocked while command palette is open
|
|
36
|
+
|
|
37
|
+
// Simulate closeCommandPalette() being called - the critical fix
|
|
38
|
+
// _commandPaletteClosing is set synchronously BEFORE the async transition
|
|
39
|
+
state._commandPaletteClosing = true;
|
|
40
|
+
state.isModalActive = false;
|
|
41
|
+
// commandPaletteInput still exists during transition
|
|
42
|
+
expect(shouldBlockNavigation()).toBe(true); // Still blocked during transition!
|
|
43
|
+
|
|
44
|
+
// After transition completes
|
|
45
|
+
state._commandPaletteClosing = false;
|
|
46
|
+
state.w.commandPaletteInput = null;
|
|
47
|
+
state.w.commandPaletteList = null;
|
|
48
|
+
state.w.commandPaletteBox = null;
|
|
49
|
+
expect(shouldBlockNavigation()).toBe(false); // Now navigation is allowed
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('should handle rapid open/close cycles', () => {
|
|
53
|
+
const state = {
|
|
54
|
+
_commandPaletteClosing: false,
|
|
55
|
+
w: {
|
|
56
|
+
commandPaletteBox: null,
|
|
57
|
+
commandPaletteInput: null,
|
|
58
|
+
commandPaletteList: null,
|
|
59
|
+
},
|
|
60
|
+
isModalActive: false,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const shouldBlockNavigation = () => {
|
|
64
|
+
return !!(state._commandPaletteClosing || (state.w.commandPaletteBox && state.w.commandPaletteInput && state.w.commandPaletteInput.focused));
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Rapid open/close cycle
|
|
68
|
+
// Open
|
|
69
|
+
state.w.commandPaletteBox = {};
|
|
70
|
+
state.w.commandPaletteInput = { focused: true };
|
|
71
|
+
state.isModalActive = true;
|
|
72
|
+
expect(shouldBlockNavigation()).toBe(true);
|
|
73
|
+
|
|
74
|
+
// Close starts
|
|
75
|
+
state._commandPaletteClosing = true;
|
|
76
|
+
state.isModalActive = false;
|
|
77
|
+
expect(shouldBlockNavigation()).toBe(true);
|
|
78
|
+
|
|
79
|
+
// Close completes
|
|
80
|
+
state._commandPaletteClosing = false;
|
|
81
|
+
state.w.commandPaletteInput = null;
|
|
82
|
+
state.w.commandPaletteBox = null;
|
|
83
|
+
expect(shouldBlockNavigation()).toBe(false);
|
|
84
|
+
|
|
85
|
+
// Rapidly re-open before any navigation
|
|
86
|
+
state.w.commandPaletteBox = {};
|
|
87
|
+
state.w.commandPaletteInput = { focused: true };
|
|
88
|
+
state.isModalActive = true;
|
|
89
|
+
expect(shouldBlockNavigation()).toBe(true);
|
|
90
|
+
|
|
91
|
+
// Close again
|
|
92
|
+
state._commandPaletteClosing = true;
|
|
93
|
+
state.isModalActive = false;
|
|
94
|
+
expect(shouldBlockNavigation()).toBe(true);
|
|
95
|
+
|
|
96
|
+
// Final cleanup
|
|
97
|
+
state._commandPaletteClosing = false;
|
|
98
|
+
state.w.commandPaletteInput = null;
|
|
99
|
+
state.w.commandPaletteBox = null;
|
|
100
|
+
expect(shouldBlockNavigation()).toBe(false);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('should use try/finally pattern to ensure flag is always cleared', () => {
|
|
104
|
+
// This test verifies the code pattern used in closeCommandPalette()
|
|
105
|
+
// The flag should be cleared even if an error occurs
|
|
106
|
+
|
|
107
|
+
const state = {
|
|
108
|
+
_commandPaletteClosing: false,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// Simulate the closeCommandPalette pattern
|
|
112
|
+
const simulateClose = async (shouldThrow = false) => {
|
|
113
|
+
state._commandPaletteClosing = true;
|
|
114
|
+
try {
|
|
115
|
+
if (shouldThrow) {
|
|
116
|
+
throw new Error('Transition failed');
|
|
117
|
+
}
|
|
118
|
+
// Simulate async transition
|
|
119
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
120
|
+
} finally {
|
|
121
|
+
state._commandPaletteClosing = false;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Normal close
|
|
126
|
+
return simulateClose(false).then(() => {
|
|
127
|
+
expect(state._commandPaletteClosing).toBe(false);
|
|
128
|
+
|
|
129
|
+
// Close with error
|
|
130
|
+
return simulateClose(true).catch(() => {
|
|
131
|
+
// Error is expected
|
|
132
|
+
});
|
|
133
|
+
}).then(() => {
|
|
134
|
+
// Flag should still be cleared even after error
|
|
135
|
+
expect(state._commandPaletteClosing).toBe(false);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('isModalActive state management', () => {
|
|
141
|
+
test('should be set false synchronously when closeCommandPalette starts', () => {
|
|
142
|
+
// This test verifies the fix for the race condition where
|
|
143
|
+
// isModalActive was only set after the async transition completed
|
|
144
|
+
|
|
145
|
+
const state = {
|
|
146
|
+
isModalActive: false,
|
|
147
|
+
_commandPaletteClosing: false,
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
// Simulate closeCommandPalette() start
|
|
151
|
+
const closeCommandPaletteStart = () => {
|
|
152
|
+
state._commandPaletteClosing = true;
|
|
153
|
+
state.isModalActive = false;
|
|
154
|
+
// At this point, even though transition hasn't completed,
|
|
155
|
+
// isModalActive is already false
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
// Open command palette first
|
|
159
|
+
state.isModalActive = true;
|
|
160
|
+
expect(state.isModalActive).toBe(true);
|
|
161
|
+
|
|
162
|
+
// Close starts - isModalActive should be false immediately
|
|
163
|
+
closeCommandPaletteStart();
|
|
164
|
+
expect(state.isModalActive).toBe(false);
|
|
165
|
+
expect(state._commandPaletteClosing).toBe(true);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('Command filtering', () => {
|
|
170
|
+
test('should filter commands by name', () => {
|
|
171
|
+
const commands = [
|
|
172
|
+
{ name: 'Toggle Help', shortcut: '?', category: 'Navigation' },
|
|
173
|
+
{ name: 'Open Settings', shortcut: 's', category: 'Navigation' },
|
|
174
|
+
{ name: 'Force Refresh', shortcut: 'r', category: 'Display' },
|
|
175
|
+
];
|
|
176
|
+
|
|
177
|
+
const filterCommands = (query) => {
|
|
178
|
+
if (!query) return commands;
|
|
179
|
+
return commands.filter(cmd =>
|
|
180
|
+
cmd.name.toLowerCase().includes(query) ||
|
|
181
|
+
cmd.shortcut.toLowerCase().includes(query) ||
|
|
182
|
+
cmd.category.toLowerCase().includes(query)
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// Filter by name
|
|
187
|
+
expect(filterCommands('help')).toHaveLength(1);
|
|
188
|
+
expect(filterCommands('help')[0].name).toBe('Toggle Help');
|
|
189
|
+
|
|
190
|
+
// Filter by shortcut (exact match)
|
|
191
|
+
expect(filterCommands('r')).toHaveLength(1);
|
|
192
|
+
expect(filterCommands('r')[0].name).toBe('Force Refresh');
|
|
193
|
+
|
|
194
|
+
// Filter by category
|
|
195
|
+
expect(filterCommands('display')).toHaveLength(1);
|
|
196
|
+
expect(filterCommands('display')[0].name).toBe('Force Refresh');
|
|
197
|
+
|
|
198
|
+
// No results
|
|
199
|
+
expect(filterCommands('xyz')).toHaveLength(0);
|
|
200
|
+
|
|
201
|
+
// Empty query returns all
|
|
202
|
+
expect(filterCommands('')).toHaveLength(3);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
test('should handle case-insensitive filtering', () => {
|
|
206
|
+
const commands = [
|
|
207
|
+
{ name: 'Toggle CPU Widget', shortcut: '1', category: 'Widgets' },
|
|
208
|
+
{ name: 'Toggle Memory Widget', shortcut: '2', category: 'Widgets' },
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
const filterCommands = (query) => {
|
|
212
|
+
if (!query) return commands;
|
|
213
|
+
const lowerQuery = query.toLowerCase();
|
|
214
|
+
return commands.filter(cmd =>
|
|
215
|
+
cmd.name.toLowerCase().includes(lowerQuery) ||
|
|
216
|
+
cmd.shortcut.toLowerCase().includes(lowerQuery) ||
|
|
217
|
+
cmd.category.toLowerCase().includes(lowerQuery)
|
|
218
|
+
);
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
// Case insensitive
|
|
222
|
+
expect(filterCommands('CPU')).toHaveLength(1);
|
|
223
|
+
expect(filterCommands('cpu')).toHaveLength(1);
|
|
224
|
+
expect(filterCommands('Cpu')).toHaveLength(1);
|
|
225
|
+
});
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
describe('Navigation guard logic', () => {
|
|
229
|
+
test('should check _commandPaletteClosing first for performance', () => {
|
|
230
|
+
// The guard uses || with _commandPaletteClosing first
|
|
231
|
+
// This means if _commandPaletteClosing is true, we don't even check commandPaletteInput.focused
|
|
232
|
+
// which could cause issues with destroyed widgets
|
|
233
|
+
|
|
234
|
+
let inputFocusedCallCount = 0;
|
|
235
|
+
const state = {
|
|
236
|
+
_commandPaletteClosing: false,
|
|
237
|
+
w: {
|
|
238
|
+
commandPaletteBox: {},
|
|
239
|
+
commandPaletteInput: {
|
|
240
|
+
get focused() {
|
|
241
|
+
inputFocusedCallCount++;
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
const shouldBlockNavigation = () => {
|
|
249
|
+
return !!(state._commandPaletteClosing || (state.w.commandPaletteBox && state.w.commandPaletteInput && state.w.commandPaletteInput.focused));
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
// When _commandPaletteClosing is false, commandPaletteInput.focused is checked
|
|
253
|
+
inputFocusedCallCount = 0;
|
|
254
|
+
state._commandPaletteClosing = false;
|
|
255
|
+
shouldBlockNavigation();
|
|
256
|
+
expect(inputFocusedCallCount).toBe(1);
|
|
257
|
+
|
|
258
|
+
// When _commandPaletteClosing is true, commandPaletteInput.focused is NOT checked (short-circuit)
|
|
259
|
+
inputFocusedCallCount = 0;
|
|
260
|
+
state._commandPaletteClosing = true;
|
|
261
|
+
shouldBlockNavigation();
|
|
262
|
+
expect(inputFocusedCallCount).toBe(0);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test('should handle null commandPaletteInput gracefully', () => {
|
|
266
|
+
const state = {
|
|
267
|
+
_commandPaletteClosing: false,
|
|
268
|
+
w: {
|
|
269
|
+
commandPaletteBox: null,
|
|
270
|
+
commandPaletteInput: null,
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
const shouldBlockNavigation = () => {
|
|
275
|
+
return !!(state._commandPaletteClosing || (state.w.commandPaletteBox && state.w.commandPaletteInput && state.w.commandPaletteInput.focused));
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
// Should not throw when commandPaletteInput is null
|
|
279
|
+
expect(() => shouldBlockNavigation()).not.toThrow();
|
|
280
|
+
expect(shouldBlockNavigation()).toBe(false);
|
|
281
|
+
|
|
282
|
+
// Should still work when _commandPaletteClosing is true
|
|
283
|
+
state._commandPaletteClosing = true;
|
|
284
|
+
expect(shouldBlockNavigation()).toBe(true);
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
describe('Command execution', () => {
|
|
289
|
+
test('should execute command action after closing modal', () => {
|
|
290
|
+
let actionExecuted = false;
|
|
291
|
+
const commands = [
|
|
292
|
+
{ name: 'Test Command', shortcut: 't', action: () => { actionExecuted = true; } }
|
|
293
|
+
];
|
|
294
|
+
|
|
295
|
+
// Simulate command execution
|
|
296
|
+
const executeCommand = (cmd) => {
|
|
297
|
+
if (cmd.action && typeof cmd.action === 'function') {
|
|
298
|
+
cmd.action();
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
executeCommand(commands[0]);
|
|
303
|
+
expect(actionExecuted).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
test('should handle commands without actions gracefully', () => {
|
|
307
|
+
const commands = [
|
|
308
|
+
{ name: 'No Action', shortcut: 'n' }
|
|
309
|
+
];
|
|
310
|
+
|
|
311
|
+
// Should not throw when action is undefined
|
|
312
|
+
expect(() => {
|
|
313
|
+
const cmd = commands[0];
|
|
314
|
+
if (cmd.action && typeof cmd.action === 'function') {
|
|
315
|
+
cmd.action();
|
|
316
|
+
}
|
|
317
|
+
}).not.toThrow();
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
});
|