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,158 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ESBuild configuration for claw-dashboard
|
|
5
|
+
* Bundles the application for distribution
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as esbuild from 'esbuild';
|
|
9
|
+
import { readFileSync, writeFileSync } from 'fs';
|
|
10
|
+
import { dirname, join } from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = dirname(__filename);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Get current version from package.json
|
|
18
|
+
* @returns {string} Current version
|
|
19
|
+
*/
|
|
20
|
+
function getVersion() {
|
|
21
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, 'package.json'), 'utf8'));
|
|
22
|
+
return pkg.version;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Create banner with version info
|
|
27
|
+
* @returns {string} Banner comment
|
|
28
|
+
*/
|
|
29
|
+
function createBanner() {
|
|
30
|
+
const version = getVersion();
|
|
31
|
+
return `#!/usr/bin/env node
|
|
32
|
+
/**
|
|
33
|
+
* claw-dashboard v${version}
|
|
34
|
+
* A beautiful console dashboard for monitoring OpenClaw instances
|
|
35
|
+
*
|
|
36
|
+
* License: MIT
|
|
37
|
+
* Repository: https://github.com/spleck/claw-dashboard
|
|
38
|
+
*/
|
|
39
|
+
`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Build configuration
|
|
44
|
+
*/
|
|
45
|
+
const buildConfig = {
|
|
46
|
+
entryPoints: ['index.js'],
|
|
47
|
+
bundle: true,
|
|
48
|
+
platform: 'node',
|
|
49
|
+
target: 'node18',
|
|
50
|
+
outfile: 'dist/clawdash',
|
|
51
|
+
format: 'esm',
|
|
52
|
+
banner: {
|
|
53
|
+
js: createBanner(),
|
|
54
|
+
},
|
|
55
|
+
// External dependencies that should not be bundled
|
|
56
|
+
// (native modules, large dependencies, or things with dynamic requires)
|
|
57
|
+
external: [
|
|
58
|
+
'blessed',
|
|
59
|
+
'blessed-contrib',
|
|
60
|
+
'systeminformation',
|
|
61
|
+
],
|
|
62
|
+
minify: true,
|
|
63
|
+
sourcemap: false,
|
|
64
|
+
metafile: true,
|
|
65
|
+
logLevel: 'info',
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Development build configuration
|
|
70
|
+
*/
|
|
71
|
+
const devConfig = {
|
|
72
|
+
...buildConfig,
|
|
73
|
+
minify: false,
|
|
74
|
+
sourcemap: true,
|
|
75
|
+
outfile: 'dist/clawdash.dev',
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Run build
|
|
80
|
+
* @param {Object} config - Build configuration
|
|
81
|
+
* @param {string} mode - Build mode (production or development)
|
|
82
|
+
*/
|
|
83
|
+
async function build(config, mode = 'production') {
|
|
84
|
+
console.log(`Building for ${mode}...`);
|
|
85
|
+
console.log(`Version: ${getVersion()}`);
|
|
86
|
+
|
|
87
|
+
try {
|
|
88
|
+
const result = await esbuild.build({
|
|
89
|
+
...config,
|
|
90
|
+
write: false,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Add shebang if not present and make executable
|
|
94
|
+
let output = result.outputFiles[0].text;
|
|
95
|
+
if (!output.startsWith('#!')) {
|
|
96
|
+
output = '#!/usr/bin/env node\n' + output;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Write the output file
|
|
100
|
+
writeFileSync(config.outfile, output, { mode: 0o755 });
|
|
101
|
+
|
|
102
|
+
// Write metafile for analysis
|
|
103
|
+
if (result.metafile) {
|
|
104
|
+
writeFileSync(
|
|
105
|
+
config.outfile + '.meta.json',
|
|
106
|
+
JSON.stringify(result.metafile, null, 2)
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Calculate bundle size
|
|
111
|
+
const bytes = Buffer.byteLength(output, 'utf8');
|
|
112
|
+
const kb = (bytes / 1024).toFixed(2);
|
|
113
|
+
|
|
114
|
+
console.log(`✓ Build successful: ${config.outfile} (${kb} KB)`);
|
|
115
|
+
|
|
116
|
+
// Print bundle analysis
|
|
117
|
+
if (result.metafile) {
|
|
118
|
+
const outputs = result.metafile.outputs;
|
|
119
|
+
const outputKey = Object.keys(outputs).find(k => k.endsWith('.js') || k.endsWith('clawdash'));
|
|
120
|
+
if (outputKey) {
|
|
121
|
+
const output = outputs[outputKey];
|
|
122
|
+
console.log(` - Bundle size: ${(output.bytes / 1024).toFixed(2)} KB`);
|
|
123
|
+
console.log(` - Exports: ${output.exports.join(', ')}`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return true;
|
|
128
|
+
} catch (error) {
|
|
129
|
+
console.error('✗ Build failed:', error.message);
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Main execution
|
|
136
|
+
*/
|
|
137
|
+
async function main() {
|
|
138
|
+
const args = process.argv.slice(2);
|
|
139
|
+
const isDev = args.includes('--dev') || args.includes('-d');
|
|
140
|
+
const shouldAnalyze = args.includes('--analyze') || args.includes('-a');
|
|
141
|
+
|
|
142
|
+
const config = isDev ? devConfig : buildConfig;
|
|
143
|
+
|
|
144
|
+
const success = await build(config, isDev ? 'development' : 'production');
|
|
145
|
+
|
|
146
|
+
if (success && shouldAnalyze) {
|
|
147
|
+
console.log('\nBundle analysis written to:', config.outfile + '.meta.json');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
process.exit(success ? 0 : 1);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Run if called directly
|
|
154
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
155
|
+
main();
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
export { build, buildConfig, devConfig };
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import js from '@eslint/js';
|
|
2
|
+
|
|
3
|
+
export default [
|
|
4
|
+
js.configs.recommended,
|
|
5
|
+
{
|
|
6
|
+
files: ['src/**/*.js'],
|
|
7
|
+
ignores: ['src/workers/*.js'],
|
|
8
|
+
languageOptions: {
|
|
9
|
+
ecmaVersion: 2022,
|
|
10
|
+
sourceType: 'module',
|
|
11
|
+
globals: {
|
|
12
|
+
console: 'readonly',
|
|
13
|
+
process: 'readonly',
|
|
14
|
+
setTimeout: 'readonly',
|
|
15
|
+
setInterval: 'readonly',
|
|
16
|
+
clearInterval: 'readonly',
|
|
17
|
+
clearTimeout: 'readonly',
|
|
18
|
+
setImmediate: 'readonly',
|
|
19
|
+
clearImmediate: 'readonly',
|
|
20
|
+
Buffer: 'readonly',
|
|
21
|
+
__dirname: 'readonly',
|
|
22
|
+
__filename: 'readonly',
|
|
23
|
+
module: 'readonly',
|
|
24
|
+
require: 'readonly',
|
|
25
|
+
exports: 'readonly',
|
|
26
|
+
global: 'readonly'
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
rules: {
|
|
30
|
+
'no-unused-vars': 'off',
|
|
31
|
+
'no-empty': 'off',
|
|
32
|
+
'no-control-regex': 'off',
|
|
33
|
+
'no-undef': 'off',
|
|
34
|
+
'semi': ['error', 'always'],
|
|
35
|
+
'quotes': ['error', 'single'],
|
|
36
|
+
'prefer-const': 'off',
|
|
37
|
+
'no-var': 'off',
|
|
38
|
+
'preserve-caught-error': 'off'
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
files: ['tests/**/*.js'],
|
|
43
|
+
languageOptions: {
|
|
44
|
+
globals: {
|
|
45
|
+
describe: 'readonly',
|
|
46
|
+
test: 'readonly',
|
|
47
|
+
expect: 'readonly',
|
|
48
|
+
beforeEach: 'readonly',
|
|
49
|
+
afterEach: 'readonly',
|
|
50
|
+
beforeAll: 'readonly',
|
|
51
|
+
afterAll: 'readonly',
|
|
52
|
+
jest: 'readonly'
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
];
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Plugin Examples
|
|
2
|
+
|
|
3
|
+
This directory contains example plugins demonstrating the Claw Dashboard widget plugin system.
|
|
4
|
+
|
|
5
|
+
## Available Examples
|
|
6
|
+
|
|
7
|
+
### hello-world
|
|
8
|
+
|
|
9
|
+
The simplest possible widget example demonstrating:
|
|
10
|
+
- Basic widget structure extending BaseWidget
|
|
11
|
+
- Required lifecycle methods (init, create, getData, render, destroy)
|
|
12
|
+
- Simple configuration handling
|
|
13
|
+
- Basic blessed UI creation
|
|
14
|
+
|
|
15
|
+
### weather-widget
|
|
16
|
+
|
|
17
|
+
A weather widget that demonstrates:
|
|
18
|
+
- Plugin manifest structure
|
|
19
|
+
- Widget class extending BaseWidget
|
|
20
|
+
- UI creation with blessed
|
|
21
|
+
- Simulated data fetching and rendering
|
|
22
|
+
- Configuration handling with defaults
|
|
23
|
+
|
|
24
|
+
### api-status
|
|
25
|
+
|
|
26
|
+
An API integration widget demonstrating:
|
|
27
|
+
- Fetching data from external APIs
|
|
28
|
+
- Error handling and retry logic
|
|
29
|
+
- Loading states and timeout handling
|
|
30
|
+
- **Environment variable configuration** (`${ENV_VAR:-default}`)
|
|
31
|
+
- **Config versioning** (`__version: "1.1.0"`)
|
|
32
|
+
- Configurable refresh intervals
|
|
33
|
+
- Request statistics tracking
|
|
34
|
+
|
|
35
|
+
### system-metrics-chart
|
|
36
|
+
|
|
37
|
+
A data visualization widget demonstrating:
|
|
38
|
+
- **blessed-contrib line charts** for time-series data
|
|
39
|
+
- Multiple metric support (CPU, memory, network)
|
|
40
|
+
- Dynamic data updates with configurable refresh
|
|
41
|
+
- History management for rolling data windows
|
|
42
|
+
- Theme integration for chart styling
|
|
43
|
+
|
|
44
|
+
## Installation
|
|
45
|
+
|
|
46
|
+
To install an example plugin:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
# Copy the plugin to the plugins directory
|
|
50
|
+
cp -r hello-world ~/.openclaw/plugins/
|
|
51
|
+
# or
|
|
52
|
+
cp -r weather-widget ~/.openclaw/plugins/
|
|
53
|
+
# or
|
|
54
|
+
cp -r api-status ~/.openclaw/plugins/
|
|
55
|
+
# or
|
|
56
|
+
cp -r system-metrics-chart ~/.openclaw/plugins/
|
|
57
|
+
|
|
58
|
+
# Restart clawdash
|
|
59
|
+
clawdash
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Example Comparison
|
|
63
|
+
|
|
64
|
+
| Example | Complexity | Demonstrates |
|
|
65
|
+
|---------|------------|--------------|
|
|
66
|
+
| `hello-world` | Beginner | Basic structure, lifecycle hooks |
|
|
67
|
+
| `weather-widget` | Beginner | Configuration, data fetching |
|
|
68
|
+
| `api-status` | Intermediate | API integration, error handling |
|
|
69
|
+
| `system-metrics-chart` | Advanced | Data visualization, charts, history |
|
|
70
|
+
|
|
71
|
+
## Creating Your Own Plugin
|
|
72
|
+
|
|
73
|
+
1. Copy the example that best matches your use case:
|
|
74
|
+
- Start with `hello-world` for simple widgets
|
|
75
|
+
- Use `weather-widget` for data-driven widgets
|
|
76
|
+
- Use `api-status` for external API integration
|
|
77
|
+
- Use `system-metrics-chart` for data visualization
|
|
78
|
+
|
|
79
|
+
2. Update `plugin.json` with your plugin details
|
|
80
|
+
3. Modify `index.js` to implement your widget logic
|
|
81
|
+
4. Install to `~/.openclaw/plugins/`
|
|
82
|
+
|
|
83
|
+
## Environment Variable Configuration
|
|
84
|
+
|
|
85
|
+
Widget configs support environment variable interpolation using the syntax:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"config": {
|
|
90
|
+
"__version": "1.0.0",
|
|
91
|
+
"apiUrl": "${API_URL:-https://default.example.com}",
|
|
92
|
+
"apiKey": "${API_KEY}",
|
|
93
|
+
"timeout": "${TIMEOUT:-5000}"
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
- `${VAR}` - Uses the environment variable value, or keeps the literal if not set
|
|
99
|
+
- `${VAR:-default}` - Uses the environment variable, or the default value if not set
|
|
100
|
+
|
|
101
|
+
Set environment variables before running clawdash:
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
export API_URL="https://api.example.com"
|
|
105
|
+
export API_KEY="your-secret-key"
|
|
106
|
+
clawdash
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Config Versioning
|
|
110
|
+
|
|
111
|
+
Widget configs can include a `__version` field to enable automatic migration when the dashboard is updated:
|
|
112
|
+
|
|
113
|
+
```json
|
|
114
|
+
{
|
|
115
|
+
"config": {
|
|
116
|
+
"__version": "1.0.0",
|
|
117
|
+
"setting": "value"
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
See the full documentation in `docs/PLUGINS.md`.
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Status Widget Plugin
|
|
3
|
+
* Demonstrates API integration patterns including:
|
|
4
|
+
* - Fetching data from external APIs
|
|
5
|
+
* - Error handling and retry logic
|
|
6
|
+
* - Loading states
|
|
7
|
+
* - Configurable refresh intervals
|
|
8
|
+
* - Timeout handling
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { BaseWidget } from '../../../src/widgets/plugin-api.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* API Status Widget - Fetches and displays data from an external API
|
|
15
|
+
*/
|
|
16
|
+
export default class ApiStatusWidget extends BaseWidget {
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
super(options);
|
|
19
|
+
this.name = options.name || 'API Status';
|
|
20
|
+
this.description = 'Fetches data from external API';
|
|
21
|
+
|
|
22
|
+
// Internal state
|
|
23
|
+
this.loading = false;
|
|
24
|
+
this.error = null;
|
|
25
|
+
this.lastFetch = null;
|
|
26
|
+
this.refreshTimer = null;
|
|
27
|
+
this.data = null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Initialize the widget
|
|
32
|
+
*/
|
|
33
|
+
async init() {
|
|
34
|
+
this.log('info', 'API Status widget initialized');
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create the widget UI
|
|
40
|
+
* @param {Object} screen - Blessed screen object
|
|
41
|
+
* @param {Object} theme - Theme colors
|
|
42
|
+
*/
|
|
43
|
+
async create(screen, theme = {}) {
|
|
44
|
+
const C = theme.colors || {};
|
|
45
|
+
const blessed = await import('blessed');
|
|
46
|
+
|
|
47
|
+
this.screen = screen;
|
|
48
|
+
this.theme = theme;
|
|
49
|
+
|
|
50
|
+
// Main container
|
|
51
|
+
this.box = blessed.default.box({
|
|
52
|
+
parent: screen,
|
|
53
|
+
width: '50%',
|
|
54
|
+
height: 7,
|
|
55
|
+
border: { type: 'line' },
|
|
56
|
+
label: ' API STATUS ',
|
|
57
|
+
style: { border: { fg: C.cyan || 'cyan' } },
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Status line (shows loading/success/error)
|
|
61
|
+
this.statusText = blessed.default.text({
|
|
62
|
+
parent: this.box,
|
|
63
|
+
top: 0,
|
|
64
|
+
left: 1,
|
|
65
|
+
content: 'Initializing...',
|
|
66
|
+
style: { fg: C.gray || 'gray' },
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Data content line
|
|
70
|
+
this.contentText = blessed.default.text({
|
|
71
|
+
parent: this.box,
|
|
72
|
+
top: 1,
|
|
73
|
+
left: 1,
|
|
74
|
+
content: '',
|
|
75
|
+
style: { fg: C.white || 'white' },
|
|
76
|
+
wrap: true,
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
// Last updated line
|
|
80
|
+
this.updatedText = blessed.default.text({
|
|
81
|
+
parent: this.box,
|
|
82
|
+
top: 3,
|
|
83
|
+
left: 1,
|
|
84
|
+
content: 'Never updated',
|
|
85
|
+
style: { fg: C.gray || 'gray' },
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Stats line (shows request count, errors)
|
|
89
|
+
this.statsText = blessed.default.text({
|
|
90
|
+
parent: this.box,
|
|
91
|
+
top: 4,
|
|
92
|
+
left: 1,
|
|
93
|
+
content: 'Requests: 0 | Errors: 0',
|
|
94
|
+
style: { fg: C.gray || 'gray' },
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
this.loaded = true;
|
|
98
|
+
this.log('debug', 'API Status widget UI created');
|
|
99
|
+
|
|
100
|
+
// Start auto-refresh if configured
|
|
101
|
+
const refreshInterval = this.config.refreshInterval || 60000;
|
|
102
|
+
if (refreshInterval > 0) {
|
|
103
|
+
this.startAutoRefresh(refreshInterval);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return this;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Fetch data from the configured API
|
|
111
|
+
* Includes retry logic and timeout handling
|
|
112
|
+
*/
|
|
113
|
+
async getData() {
|
|
114
|
+
const apiUrl = this.config.apiUrl || 'https://api.github.com/zen';
|
|
115
|
+
const timeout = this.config.timeout || 5000;
|
|
116
|
+
const maxRetries = this.config.retries || 3;
|
|
117
|
+
|
|
118
|
+
let lastError = null;
|
|
119
|
+
|
|
120
|
+
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
121
|
+
try {
|
|
122
|
+
this.loading = true;
|
|
123
|
+
this.error = null;
|
|
124
|
+
this.updateStatus('loading', `Fetching (attempt ${attempt}/${maxRetries})...`);
|
|
125
|
+
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
128
|
+
|
|
129
|
+
const response = await fetch(apiUrl, {
|
|
130
|
+
signal: controller.signal,
|
|
131
|
+
headers: {
|
|
132
|
+
'User-Agent': 'Claw-Dashboard-Widget/1.0',
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
clearTimeout(timeoutId);
|
|
137
|
+
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Handle different response types
|
|
143
|
+
const contentType = response.headers.get('content-type') || '';
|
|
144
|
+
let data;
|
|
145
|
+
|
|
146
|
+
if (contentType.includes('application/json')) {
|
|
147
|
+
data = await response.json();
|
|
148
|
+
} else {
|
|
149
|
+
data = await response.text();
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
this.loading = false;
|
|
153
|
+
this.lastFetch = new Date();
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
success: true,
|
|
157
|
+
data,
|
|
158
|
+
timestamp: this.lastFetch.toISOString(),
|
|
159
|
+
apiUrl,
|
|
160
|
+
};
|
|
161
|
+
} catch (err) {
|
|
162
|
+
lastError = err;
|
|
163
|
+
this.log('warn', `API fetch attempt ${attempt} failed: ${err.message}`);
|
|
164
|
+
|
|
165
|
+
// Don't retry on abort (timeout)
|
|
166
|
+
if (err.name === 'AbortError') {
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Wait before retrying (exponential backoff)
|
|
171
|
+
if (attempt < maxRetries) {
|
|
172
|
+
await new Promise(resolve => setTimeout(resolve, 1000 * attempt));
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// All retries failed
|
|
178
|
+
this.loading = false;
|
|
179
|
+
this.error = lastError;
|
|
180
|
+
|
|
181
|
+
return {
|
|
182
|
+
success: false,
|
|
183
|
+
error: lastError?.message || 'Unknown error',
|
|
184
|
+
timestamp: new Date().toISOString(),
|
|
185
|
+
apiUrl,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Render the widget with fetched data
|
|
191
|
+
* @param {Object} result - Result from getData()
|
|
192
|
+
*/
|
|
193
|
+
render(result) {
|
|
194
|
+
if (!this.box) return;
|
|
195
|
+
|
|
196
|
+
if (result.success) {
|
|
197
|
+
this.updateStatus('success', 'Connected');
|
|
198
|
+
|
|
199
|
+
// Format the data for display
|
|
200
|
+
let content;
|
|
201
|
+
if (typeof result.data === 'object') {
|
|
202
|
+
content = JSON.stringify(result.data, null, 0).slice(0, 100);
|
|
203
|
+
} else {
|
|
204
|
+
content = String(result.data).slice(0, 100);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
this.contentText.setContent(content);
|
|
208
|
+
this.contentText.style.fg = this.theme?.colors?.white || 'white';
|
|
209
|
+
} else {
|
|
210
|
+
this.updateStatus('error', `Error: ${result.error}`);
|
|
211
|
+
this.contentText.setContent('Unable to fetch data');
|
|
212
|
+
this.contentText.style.fg = this.theme?.colors?.red || 'red';
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Update timestamp
|
|
216
|
+
this.updatedText.setContent(`Last: ${result.timestamp || 'Never'}`);
|
|
217
|
+
|
|
218
|
+
// Update stats
|
|
219
|
+
const stats = this.getStats();
|
|
220
|
+
this.statsText.setContent(`Requests: ${stats.requests} | Errors: ${stats.errors}`);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Update the status indicator
|
|
225
|
+
* @private
|
|
226
|
+
*/
|
|
227
|
+
updateStatus(status, message) {
|
|
228
|
+
const colors = {
|
|
229
|
+
loading: this.theme?.colors?.yellow || 'yellow',
|
|
230
|
+
success: this.theme?.colors?.green || 'green',
|
|
231
|
+
error: this.theme?.colors?.red || 'red',
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
this.statusText.setContent(message);
|
|
235
|
+
this.statusText.style.fg = colors[status] || 'white';
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Track request statistics
|
|
240
|
+
* @private
|
|
241
|
+
*/
|
|
242
|
+
getStats() {
|
|
243
|
+
if (!this._stats) {
|
|
244
|
+
this._stats = { requests: 0, errors: 0 };
|
|
245
|
+
}
|
|
246
|
+
return this._stats;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Start auto-refresh timer
|
|
251
|
+
* @param {number} intervalMs - Refresh interval in milliseconds
|
|
252
|
+
*/
|
|
253
|
+
startAutoRefresh(intervalMs) {
|
|
254
|
+
this.stopAutoRefresh();
|
|
255
|
+
this.refreshTimer = setInterval(() => {
|
|
256
|
+
if (!this.loading) {
|
|
257
|
+
this.getData()
|
|
258
|
+
.then(data => this.render(data))
|
|
259
|
+
.catch(err => this.log('error', `Auto-refresh failed: ${err.message}`));
|
|
260
|
+
}
|
|
261
|
+
}, intervalMs);
|
|
262
|
+
|
|
263
|
+
this.log('debug', `Auto-refresh started (${intervalMs}ms interval)`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Stop auto-refresh timer
|
|
268
|
+
*/
|
|
269
|
+
stopAutoRefresh() {
|
|
270
|
+
if (this.refreshTimer) {
|
|
271
|
+
clearInterval(this.refreshTimer);
|
|
272
|
+
this.refreshTimer = null;
|
|
273
|
+
this.log('debug', 'Auto-refresh stopped');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Destroy the widget
|
|
279
|
+
*/
|
|
280
|
+
async destroy() {
|
|
281
|
+
this.stopAutoRefresh();
|
|
282
|
+
|
|
283
|
+
if (this.box) {
|
|
284
|
+
this.box.destroy();
|
|
285
|
+
this.box = null;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
this.loaded = false;
|
|
289
|
+
this.log('info', 'API Status widget destroyed');
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Export named export for flexibility
|
|
294
|
+
export { ApiStatusWidget };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "example-api-status",
|
|
3
|
+
"name": "API Status",
|
|
4
|
+
"description": "Example widget demonstrating API integration with error handling, loading states, and environment variable configuration",
|
|
5
|
+
"version": "1.1.0",
|
|
6
|
+
"author": "Claw Dashboard Team",
|
|
7
|
+
"category": "example",
|
|
8
|
+
"type": "widget",
|
|
9
|
+
"lazyLoad": true,
|
|
10
|
+
"priority": 100,
|
|
11
|
+
"config": {
|
|
12
|
+
"__version": "1.1.0",
|
|
13
|
+
"apiUrl": "${API_STATUS_URL:-https://api.github.com/zen}",
|
|
14
|
+
"apiKey": "${API_STATUS_KEY:-}",
|
|
15
|
+
"refreshInterval": "${API_STATUS_REFRESH:-60000}",
|
|
16
|
+
"timeout": "${API_STATUS_TIMEOUT:-5000}",
|
|
17
|
+
"retries": "${API_STATUS_RETRIES:-3}"
|
|
18
|
+
}
|
|
19
|
+
}
|