claw-dashboard 2.0.0 → 2.1.1
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/.github/workflows/ci.yml +10 -10
- package/.github/workflows/release.yml +2 -2
- package/.github/workflows/security.yml +7 -7
- package/index.js +54 -5
- package/package.json +4 -4
- package/src/plugin-scaffold.js +64 -71
- package/src/widgets/builtin-widgets.js +0 -95
- package/src/widgets/index.js +0 -1
- package/tests/removed-dependencies.test.js +74 -0
package/.github/workflows/ci.yml
CHANGED
|
@@ -25,10 +25,10 @@ jobs:
|
|
|
25
25
|
|
|
26
26
|
steps:
|
|
27
27
|
- name: Checkout code
|
|
28
|
-
uses: actions/checkout@
|
|
28
|
+
uses: actions/checkout@v6
|
|
29
29
|
|
|
30
30
|
- name: Setup Node.js ${{ matrix.node-version }}
|
|
31
|
-
uses: actions/setup-node@
|
|
31
|
+
uses: actions/setup-node@v6
|
|
32
32
|
with:
|
|
33
33
|
node-version: ${{ matrix.node-version }}
|
|
34
34
|
cache: 'npm'
|
|
@@ -48,7 +48,7 @@ jobs:
|
|
|
48
48
|
|
|
49
49
|
- name: Upload coverage to Codecov
|
|
50
50
|
if: matrix.node-version == 20 && github.event_name != 'pull_request'
|
|
51
|
-
uses: codecov/codecov-action@
|
|
51
|
+
uses: codecov/codecov-action@v5
|
|
52
52
|
with:
|
|
53
53
|
files: ./coverage/lcov.info
|
|
54
54
|
fail_ci_if_error: false
|
|
@@ -63,10 +63,10 @@ jobs:
|
|
|
63
63
|
|
|
64
64
|
steps:
|
|
65
65
|
- name: Checkout code
|
|
66
|
-
uses: actions/checkout@
|
|
66
|
+
uses: actions/checkout@v6
|
|
67
67
|
|
|
68
68
|
- name: Setup Node.js
|
|
69
|
-
uses: actions/setup-node@
|
|
69
|
+
uses: actions/setup-node@v6
|
|
70
70
|
with:
|
|
71
71
|
node-version: 20
|
|
72
72
|
cache: 'npm'
|
|
@@ -78,7 +78,7 @@ jobs:
|
|
|
78
78
|
run: npm run build
|
|
79
79
|
|
|
80
80
|
- name: Upload build artifacts
|
|
81
|
-
uses: actions/upload-artifact@
|
|
81
|
+
uses: actions/upload-artifact@v7
|
|
82
82
|
with:
|
|
83
83
|
name: dist
|
|
84
84
|
path: dist/
|
|
@@ -92,7 +92,7 @@ jobs:
|
|
|
92
92
|
|
|
93
93
|
steps:
|
|
94
94
|
- name: Checkout code
|
|
95
|
-
uses: actions/checkout@
|
|
95
|
+
uses: actions/checkout@v6
|
|
96
96
|
|
|
97
97
|
- name: Set up Docker Buildx
|
|
98
98
|
uses: docker/setup-buildx-action@v3
|
|
@@ -117,10 +117,10 @@ jobs:
|
|
|
117
117
|
|
|
118
118
|
steps:
|
|
119
119
|
- name: Checkout code
|
|
120
|
-
uses: actions/checkout@
|
|
120
|
+
uses: actions/checkout@v6
|
|
121
121
|
|
|
122
122
|
- name: Setup Node.js
|
|
123
|
-
uses: actions/setup-node@
|
|
123
|
+
uses: actions/setup-node@v6
|
|
124
124
|
with:
|
|
125
125
|
node-version: 20
|
|
126
126
|
cache: 'npm'
|
|
@@ -141,7 +141,7 @@ jobs:
|
|
|
141
141
|
output: 'trivy-results.sarif'
|
|
142
142
|
|
|
143
143
|
- name: Upload Trivy scan results
|
|
144
|
-
uses: github/codeql-action/upload-sarif@
|
|
144
|
+
uses: github/codeql-action/upload-sarif@v4
|
|
145
145
|
if: always()
|
|
146
146
|
with:
|
|
147
147
|
sarif_file: 'trivy-results.sarif'
|
|
@@ -17,10 +17,10 @@ jobs:
|
|
|
17
17
|
|
|
18
18
|
steps:
|
|
19
19
|
- name: Checkout code
|
|
20
|
-
uses: actions/checkout@
|
|
20
|
+
uses: actions/checkout@v6
|
|
21
21
|
|
|
22
22
|
- name: Setup Node.js
|
|
23
|
-
uses: actions/setup-node@
|
|
23
|
+
uses: actions/setup-node@v6
|
|
24
24
|
with:
|
|
25
25
|
node-version: 20
|
|
26
26
|
cache: 'npm'
|
|
@@ -45,7 +45,7 @@ jobs:
|
|
|
45
45
|
|
|
46
46
|
steps:
|
|
47
47
|
- name: Checkout code
|
|
48
|
-
uses: actions/checkout@
|
|
48
|
+
uses: actions/checkout@v6
|
|
49
49
|
|
|
50
50
|
- name: Dependency Review
|
|
51
51
|
uses: actions/dependency-review-action@v4
|
|
@@ -68,17 +68,17 @@ jobs:
|
|
|
68
68
|
|
|
69
69
|
steps:
|
|
70
70
|
- name: Checkout code
|
|
71
|
-
uses: actions/checkout@
|
|
71
|
+
uses: actions/checkout@v6
|
|
72
72
|
|
|
73
73
|
- name: Initialize CodeQL
|
|
74
|
-
uses: github/codeql-action/init@
|
|
74
|
+
uses: github/codeql-action/init@v4
|
|
75
75
|
with:
|
|
76
76
|
languages: ${{ matrix.language }}
|
|
77
77
|
|
|
78
78
|
- name: Autobuild
|
|
79
|
-
uses: github/codeql-action/autobuild@
|
|
79
|
+
uses: github/codeql-action/autobuild@v4
|
|
80
80
|
|
|
81
81
|
- name: Perform CodeQL Analysis
|
|
82
|
-
uses: github/codeql-action/analyze@
|
|
82
|
+
uses: github/codeql-action/analyze@v4
|
|
83
83
|
with:
|
|
84
84
|
category: "/language:${{matrix.language}}"
|
package/index.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import blessed from 'blessed';
|
|
4
|
-
import contrib from 'blessed-contrib';
|
|
5
4
|
import si from 'systeminformation';
|
|
6
5
|
import { exec } from 'child_process';
|
|
7
6
|
import { promisify } from 'util';
|
|
@@ -397,6 +396,22 @@ async function getLatestVersion() {
|
|
|
397
396
|
} catch { return null; }
|
|
398
397
|
}
|
|
399
398
|
|
|
399
|
+
async function getLatestDashboardVersion() {
|
|
400
|
+
try {
|
|
401
|
+
return await new Promise((resolve) => {
|
|
402
|
+
https.get('https://api.github.com/repos/spleck/claw-dashboard/releases/latest', {
|
|
403
|
+
headers: { 'User-Agent': 'claw-dashboard' }
|
|
404
|
+
}, (res) => {
|
|
405
|
+
let data = '';
|
|
406
|
+
res.on('data', chunk => data += chunk);
|
|
407
|
+
res.on('end', () => {
|
|
408
|
+
try { resolve(JSON.parse(data).tag_name?.replace(/^v/, '')); } catch { resolve(null); }
|
|
409
|
+
});
|
|
410
|
+
}).on('error', () => resolve(null)).setTimeout(3000);
|
|
411
|
+
});
|
|
412
|
+
} catch { return null; }
|
|
413
|
+
}
|
|
414
|
+
|
|
400
415
|
function formatDuration(seconds) {
|
|
401
416
|
if (!seconds || seconds < 0) return '--';
|
|
402
417
|
const days = Math.floor(seconds / 86400);
|
|
@@ -836,7 +851,7 @@ class Dashboard {
|
|
|
836
851
|
// Favorites state
|
|
837
852
|
this.showFavoritesOnly = this.settings.showFavoritesOnly || false;
|
|
838
853
|
this.history = { cpu: new Array(HISTORY_LENGTH).fill(0), memory: new Array(HISTORY_LENGTH).fill(0), netRx: new Array(NETWORK_HISTORY_LENGTH).fill(0), netTx: new Array(NETWORK_HISTORY_LENGTH).fill(0) };
|
|
839
|
-
this.data = { cpu: [], memory: {}, openclaw: null, gpu: null, network: null, sessions: [], agents: [], version: null, latest: null, sessionTPS: {}, sessionLastTPS: {} };
|
|
854
|
+
this.data = { cpu: [], memory: {}, openclaw: null, gpu: null, network: null, sessions: [], agents: [], version: null, latest: null, dashboardVersion: DASHBOARD_VERSION, dashboardLatest: null, sessionTPS: {}, sessionLastTPS: {} };
|
|
840
855
|
// Data freshness tracking - stores timestamps when each data type was last successfully fetched
|
|
841
856
|
this.dataTimestamps = { cpu: null, memory: null, gpu: null, network: null, disk: null, system: null, sessions: null };
|
|
842
857
|
this.prev = null;
|
|
@@ -1265,6 +1280,13 @@ class Dashboard {
|
|
|
1265
1280
|
}).catch(() => {
|
|
1266
1281
|
this.data.latest = null;
|
|
1267
1282
|
});
|
|
1283
|
+
|
|
1284
|
+
// Also check for dashboard updates
|
|
1285
|
+
getLatestDashboardVersion().then(latest => {
|
|
1286
|
+
this.data.dashboardLatest = latest;
|
|
1287
|
+
}).catch(() => {
|
|
1288
|
+
this.data.dashboardLatest = null;
|
|
1289
|
+
});
|
|
1268
1290
|
}
|
|
1269
1291
|
|
|
1270
1292
|
createWidgets() {
|
|
@@ -5081,11 +5103,23 @@ class Dashboard {
|
|
|
5081
5103
|
}
|
|
5082
5104
|
}
|
|
5083
5105
|
|
|
5106
|
+
// Fetch gateway uptime FIRST - if process isn't running, gateway is down
|
|
5107
|
+
// This must be checked before fetchSessions() since it can succeed via local fallback
|
|
5108
|
+
this.data.gatewayUptime = await getGatewayUptime();
|
|
5109
|
+
const isGatewayProcessRunning = this.data.gatewayUptime !== null;
|
|
5110
|
+
|
|
5084
5111
|
// Fetch sessions via API (same as clawps) - has displayName and channel
|
|
5085
5112
|
try {
|
|
5086
5113
|
const sessions = await timeOperation('Sessions', () => this.fetchSessions(), 5000);
|
|
5087
5114
|
this.data.sessions = sessions || [];
|
|
5088
|
-
|
|
5115
|
+
// Gateway is ONLY reachable if: process is running AND at least one endpoint responds
|
|
5116
|
+
const stats = this.data.gatewayStats;
|
|
5117
|
+
const allUnreachable = stats && stats.totalEndpoints > 0 && stats.reachableEndpoints === 0;
|
|
5118
|
+
if (!isGatewayProcessRunning || allUnreachable) {
|
|
5119
|
+
this.data.openclaw = { gateway: { reachable: false } };
|
|
5120
|
+
} else {
|
|
5121
|
+
this.data.openclaw = { gateway: { reachable: true } };
|
|
5122
|
+
}
|
|
5089
5123
|
this.dataTimestamps.sessions = now;
|
|
5090
5124
|
|
|
5091
5125
|
// Clean up stale sessionTPS entries for sessions that no longer exist
|
|
@@ -5437,7 +5471,18 @@ class Dashboard {
|
|
|
5437
5471
|
openclawText = `openclaw ${current}`;
|
|
5438
5472
|
}
|
|
5439
5473
|
}
|
|
5440
|
-
|
|
5474
|
+
|
|
5475
|
+
// Render dashboard version with update check
|
|
5476
|
+
let dashboardText = `dashboard ${this.data.dashboardVersion}`;
|
|
5477
|
+
if (this.data.dashboardLatest && this.data.dashboardVersion) {
|
|
5478
|
+
if (this.data.dashboardVersion === this.data.dashboardLatest) {
|
|
5479
|
+
dashboardText = `dashboard ${this.data.dashboardVersion} ✓`;
|
|
5480
|
+
} else {
|
|
5481
|
+
dashboardText = `dashboard ${this.data.dashboardVersion} → ${this.data.dashboardLatest}`;
|
|
5482
|
+
}
|
|
5483
|
+
}
|
|
5484
|
+
|
|
5485
|
+
this.diffRenderer.setContent('title', this.w.title, `Claw ${dashboardText}, ${openclawText}`);
|
|
5441
5486
|
|
|
5442
5487
|
// Update clock - show current local time, PAUSED indicator on the right (with differential updates)
|
|
5443
5488
|
const now = new Date();
|
|
@@ -5588,9 +5633,12 @@ class Dashboard {
|
|
|
5588
5633
|
const gatewayHealth = gatewayManager.getEndpointHealth();
|
|
5589
5634
|
const unreachableCount = gatewayHealth.filter(ep => ep.enabled && !ep.reachable).length;
|
|
5590
5635
|
const enabledCount = gatewayHealth.filter(ep => ep.enabled).length;
|
|
5636
|
+
const isGatewayProcessRunning = this.data.gatewayUptime !== null;
|
|
5591
5637
|
let gatewayIndicator = '';
|
|
5592
5638
|
if (enabledCount > 0) {
|
|
5593
|
-
if (
|
|
5639
|
+
if (!isGatewayProcessRunning) {
|
|
5640
|
+
gatewayIndicator = '{red-fg}✗ gateway offline{/red-fg} [G] retry ';
|
|
5641
|
+
} else if (unreachableCount === 0) {
|
|
5594
5642
|
gatewayIndicator = '{green-fg}● gateway{/green-fg} ';
|
|
5595
5643
|
} else if (unreachableCount === enabledCount) {
|
|
5596
5644
|
gatewayIndicator = '{red-fg}✗ gateway offline{/red-fg} [G] retry ';
|
|
@@ -5925,5 +5973,6 @@ async function main() {
|
|
|
5925
5973
|
// Run main
|
|
5926
5974
|
main().catch(err => {
|
|
5927
5975
|
console.error('Fatal error:', err);
|
|
5976
|
+
if (err.stack) console.error('Stack:', err.stack);
|
|
5928
5977
|
process.exit(1);
|
|
5929
5978
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claw-dashboard",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"description": "A beautiful console dashboard for monitoring OpenClaw instances — inspired by htop/btop/mactop",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"exports": {
|
|
@@ -60,8 +60,8 @@
|
|
|
60
60
|
"node": ">=18"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
-
"blessed": "^0.1.81",
|
|
64
|
-
"blessed
|
|
63
|
+
"@pm2/blessed": "^0.1.81",
|
|
64
|
+
"blessed": "npm:@pm2/blessed@^0.1.81",
|
|
65
65
|
"chalk": "^5.3.0",
|
|
66
66
|
"sql.js": "^1.14.0",
|
|
67
67
|
"systeminformation": "^5.21.22"
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@eslint/js": "^10.0.1",
|
|
71
71
|
"c8": "^11.0.0",
|
|
72
|
-
"esbuild": "^0.
|
|
72
|
+
"esbuild": "^0.27.3",
|
|
73
73
|
"eslint": "^10.0.2",
|
|
74
74
|
"husky": "^9.1.7",
|
|
75
75
|
"jest": "^30.2.0",
|
package/src/plugin-scaffold.js
CHANGED
|
@@ -585,7 +585,7 @@ export { ${className} };
|
|
|
585
585
|
|
|
586
586
|
chart: {
|
|
587
587
|
name: 'Chart Widget',
|
|
588
|
-
description: 'Widget with real-time line chart visualization using
|
|
588
|
+
description: 'Widget with real-time line chart visualization using ASCII art',
|
|
589
589
|
manifest: (id, name, author, options = {}) => ({
|
|
590
590
|
id,
|
|
591
591
|
name: name || id.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' '),
|
|
@@ -607,7 +607,7 @@ export { ${className} };
|
|
|
607
607
|
|
|
608
608
|
widgetCode: (id, className) => `/**
|
|
609
609
|
* ${className} Widget Plugin
|
|
610
|
-
* Chart widget with real-time data visualization using
|
|
610
|
+
* Chart widget with real-time data visualization using ASCII art
|
|
611
611
|
*/
|
|
612
612
|
|
|
613
613
|
import { BaseWidget } from 'claw-dashboard/widgets';
|
|
@@ -644,14 +644,13 @@ export default class ${className} extends BaseWidget {
|
|
|
644
644
|
}
|
|
645
645
|
|
|
646
646
|
/**
|
|
647
|
-
* Create the widget UI with
|
|
647
|
+
* Create the widget UI with ASCII line chart
|
|
648
648
|
* @param {Object} screen - Blessed screen object
|
|
649
649
|
* @param {Object} theme - Theme colors
|
|
650
650
|
*/
|
|
651
651
|
async create(screen, theme = {}) {
|
|
652
652
|
const C = theme.colors || {};
|
|
653
653
|
const blessed = await import('blessed');
|
|
654
|
-
const contrib = await import('blessed-contrib');
|
|
655
654
|
|
|
656
655
|
this.screen = screen;
|
|
657
656
|
this.theme = theme;
|
|
@@ -668,27 +667,15 @@ export default class ${className} extends BaseWidget {
|
|
|
668
667
|
},
|
|
669
668
|
});
|
|
670
669
|
|
|
671
|
-
// Create
|
|
672
|
-
this.
|
|
670
|
+
// Create ASCII chart area using text element
|
|
671
|
+
this.chartArea = blessed.default.text({
|
|
673
672
|
parent: this.box,
|
|
674
673
|
top: 1,
|
|
675
674
|
left: 1,
|
|
676
675
|
width: '95%',
|
|
677
|
-
height:
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
text: C.white || 'white',
|
|
681
|
-
baseline: C.gray || 'gray',
|
|
682
|
-
},
|
|
683
|
-
xLabelPadding: 3,
|
|
684
|
-
xPadding: 5,
|
|
685
|
-
numYLabels: 5,
|
|
686
|
-
showNthLabel: Math.ceil(this.maxDataPoints / 6),
|
|
687
|
-
showLegend: this.showLegend,
|
|
688
|
-
legend: { width: 12 },
|
|
689
|
-
minY: 0,
|
|
690
|
-
maxY: 100,
|
|
691
|
-
wholeNumbersOnly: true,
|
|
676
|
+
height: 13,
|
|
677
|
+
tags: true,
|
|
678
|
+
style: { fg: C.green || 'green' },
|
|
692
679
|
});
|
|
693
680
|
|
|
694
681
|
// Add info text
|
|
@@ -709,6 +696,29 @@ export default class ${className} extends BaseWidget {
|
|
|
709
696
|
return this;
|
|
710
697
|
}
|
|
711
698
|
|
|
699
|
+
/**
|
|
700
|
+
* Generate ASCII chart from data
|
|
701
|
+
*/
|
|
702
|
+
_renderAsciiChart(values, width = 60, height = 10) {
|
|
703
|
+
if (!values || values.length === 0) return 'No data';
|
|
704
|
+
|
|
705
|
+
const min = Math.min(...values);
|
|
706
|
+
const max = Math.max(...values);
|
|
707
|
+
const range = max - min || 1;
|
|
708
|
+
|
|
709
|
+
let chart = '';
|
|
710
|
+
for (let row = height - 1; row >= 0; row--) {
|
|
711
|
+
const threshold = min + (range * row / height);
|
|
712
|
+
let line = '';
|
|
713
|
+
for (let i = 0; i < Math.min(values.length, width); i++) {
|
|
714
|
+
const val = values[i];
|
|
715
|
+
line += val >= threshold ? '█' : '░';
|
|
716
|
+
}
|
|
717
|
+
chart += line + '\n';
|
|
718
|
+
}
|
|
719
|
+
return chart;
|
|
720
|
+
}
|
|
721
|
+
|
|
712
722
|
/**
|
|
713
723
|
* Generate sample data - customize this for your data source
|
|
714
724
|
*/
|
|
@@ -748,16 +758,9 @@ export default class ${className} extends BaseWidget {
|
|
|
748
758
|
render(data) {
|
|
749
759
|
if (!this.chart || !data) return;
|
|
750
760
|
|
|
751
|
-
//
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
x: data.labels,
|
|
755
|
-
y: data.values,
|
|
756
|
-
style: { line: 'green' },
|
|
757
|
-
};
|
|
758
|
-
|
|
759
|
-
// Update the chart
|
|
760
|
-
this.chart.setData([chartData]);
|
|
761
|
+
// Generate ASCII chart
|
|
762
|
+
const asciiChart = this._renderAsciiChart(data.values);
|
|
763
|
+
this.chartArea.setContent(asciiChart);
|
|
761
764
|
|
|
762
765
|
// Update info text
|
|
763
766
|
const avg = data.values.length > 0
|
|
@@ -1102,7 +1105,6 @@ export default class ${className} extends BaseWidget {
|
|
|
1102
1105
|
async create(screen, theme = {}) {
|
|
1103
1106
|
const C = theme.colors || {};
|
|
1104
1107
|
const blessed = await import('blessed');
|
|
1105
|
-
const contrib = await import('blessed-contrib');
|
|
1106
1108
|
|
|
1107
1109
|
this.screen = screen;
|
|
1108
1110
|
this.theme = theme;
|
|
@@ -1119,35 +1121,16 @@ export default class ${className} extends BaseWidget {
|
|
|
1119
1121
|
},
|
|
1120
1122
|
});
|
|
1121
1123
|
|
|
1122
|
-
// Create gauge
|
|
1123
|
-
|
|
1124
|
-
this.
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
value: { fg: C.green || 'green' },
|
|
1133
|
-
},
|
|
1134
|
-
label: this.name,
|
|
1135
|
-
});
|
|
1136
|
-
} else {
|
|
1137
|
-
// Linear gauge
|
|
1138
|
-
this.gauge = contrib.default.gauge({
|
|
1139
|
-
parent: this.box,
|
|
1140
|
-
top: 2,
|
|
1141
|
-
left: 1,
|
|
1142
|
-
width: '96%',
|
|
1143
|
-
height: 4,
|
|
1144
|
-
style: {
|
|
1145
|
-
label: { fg: C.white || 'white' },
|
|
1146
|
-
value: { fg: C.green || 'green' },
|
|
1147
|
-
},
|
|
1148
|
-
label: this.name,
|
|
1149
|
-
});
|
|
1150
|
-
}
|
|
1124
|
+
// Create ASCII gauge area
|
|
1125
|
+
this.gaugeArea = blessed.default.text({
|
|
1126
|
+
parent: this.box,
|
|
1127
|
+
top: 1,
|
|
1128
|
+
left: 'center',
|
|
1129
|
+
width: '90%',
|
|
1130
|
+
height: 6,
|
|
1131
|
+
tags: true,
|
|
1132
|
+
style: { fg: C.green || 'green' },
|
|
1133
|
+
});
|
|
1151
1134
|
|
|
1152
1135
|
// Value display
|
|
1153
1136
|
this.valueText = blessed.default.text({
|
|
@@ -1167,6 +1150,15 @@ export default class ${className} extends BaseWidget {
|
|
|
1167
1150
|
return this;
|
|
1168
1151
|
}
|
|
1169
1152
|
|
|
1153
|
+
/**
|
|
1154
|
+
* Generate ASCII gauge
|
|
1155
|
+
*/
|
|
1156
|
+
_renderAsciiGauge(percent, width = 20) {
|
|
1157
|
+
const filled = Math.round((percent / 100) * width);
|
|
1158
|
+
const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
1159
|
+
return bar;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1170
1162
|
/**
|
|
1171
1163
|
* Generate sample data - customize this for your data source
|
|
1172
1164
|
*/
|
|
@@ -1186,14 +1178,11 @@ export default class ${className} extends BaseWidget {
|
|
|
1186
1178
|
* Render the gauge with data
|
|
1187
1179
|
*/
|
|
1188
1180
|
render(result) {
|
|
1189
|
-
if (!this.
|
|
1190
|
-
|
|
1191
|
-
// Set gauge percentage (0-100)
|
|
1192
|
-
this.gauge.setData(result.percentage);
|
|
1193
|
-
|
|
1194
|
-
// Update value text
|
|
1195
|
-
this.valueText.setContent(result.value + this.unit);
|
|
1181
|
+
if (!this.gaugeArea || !result) return;
|
|
1196
1182
|
|
|
1183
|
+
// Generate ASCII gauge
|
|
1184
|
+
const asciiGauge = this._renderAsciiGauge(result.percentage);
|
|
1185
|
+
|
|
1197
1186
|
// Color based on value
|
|
1198
1187
|
let color = 'green';
|
|
1199
1188
|
if (result.percentage > 80) {
|
|
@@ -1202,6 +1191,11 @@ export default class ${className} extends BaseWidget {
|
|
|
1202
1191
|
color = 'yellow';
|
|
1203
1192
|
}
|
|
1204
1193
|
|
|
1194
|
+
// Update gauge display
|
|
1195
|
+
this.gaugeArea.setContent('{' + color + '-fg}' + asciiGauge + '{/' + color + '-fg}');
|
|
1196
|
+
|
|
1197
|
+
// Update value text
|
|
1198
|
+
this.valueText.setContent(result.value + this.unit);
|
|
1205
1199
|
this.valueText.style.fg = this.theme?.colors?.[color] || color;
|
|
1206
1200
|
}
|
|
1207
1201
|
|
|
@@ -1242,9 +1236,8 @@ export default class ${className} extends BaseWidget {
|
|
|
1242
1236
|
async destroy() {
|
|
1243
1237
|
this.stopAutoRefresh();
|
|
1244
1238
|
|
|
1245
|
-
if (this.
|
|
1246
|
-
this.
|
|
1247
|
-
this.gauge = null;
|
|
1239
|
+
if (this.gaugeArea) {
|
|
1240
|
+
this.gaugeArea = null;
|
|
1248
1241
|
}
|
|
1249
1242
|
|
|
1250
1243
|
if (this.box) {
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import blessed from 'blessed';
|
|
7
|
-
import contrib from 'blessed-contrib';
|
|
8
7
|
import { BaseWidget } from './plugin-api.js';
|
|
9
8
|
|
|
10
9
|
/**
|
|
@@ -239,98 +238,6 @@ export class GpuWidget extends BaseWidget {
|
|
|
239
238
|
}
|
|
240
239
|
}
|
|
241
240
|
|
|
242
|
-
/**
|
|
243
|
-
* Network Widget - Displays network activity
|
|
244
|
-
*/
|
|
245
|
-
export class NetworkWidget extends BaseWidget {
|
|
246
|
-
constructor(options = {}) {
|
|
247
|
-
super(options);
|
|
248
|
-
this.name = 'Network';
|
|
249
|
-
this.description = 'Network activity';
|
|
250
|
-
this.history = [];
|
|
251
|
-
this.maxHistory = 30;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
async create(screen, theme = {}) {
|
|
255
|
-
const C = theme.colors || {};
|
|
256
|
-
|
|
257
|
-
this.box = blessed.box({
|
|
258
|
-
parent: screen,
|
|
259
|
-
height: 5,
|
|
260
|
-
border: { type: 'line' },
|
|
261
|
-
label: ' NETWORK ',
|
|
262
|
-
style: { border: { fg: C.brightCyan || 'bright-cyan' } },
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
this.valueText = blessed.text({
|
|
266
|
-
parent: this.box,
|
|
267
|
-
top: 0,
|
|
268
|
-
left: 'center',
|
|
269
|
-
content: 'Loading...',
|
|
270
|
-
style: { fg: C.brightCyan || 'bright-cyan', bold: true },
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
this.detailText = blessed.text({
|
|
274
|
-
parent: this.box,
|
|
275
|
-
top: 1,
|
|
276
|
-
left: 'center',
|
|
277
|
-
content: '',
|
|
278
|
-
style: { fg: C.gray || 'gray' },
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
this.sparkline = contrib.sparkline({
|
|
282
|
-
parent: this.box,
|
|
283
|
-
top: 2,
|
|
284
|
-
left: 'center',
|
|
285
|
-
width: 20,
|
|
286
|
-
height: 1,
|
|
287
|
-
style: { fg: C.cyan || 'cyan' },
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
return this;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
async getData(dataProvider) {
|
|
294
|
-
if (dataProvider) {
|
|
295
|
-
return dataProvider('network');
|
|
296
|
-
}
|
|
297
|
-
return null;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
formatBytes(bytes) {
|
|
301
|
-
if (bytes === 0) return '0 B';
|
|
302
|
-
const k = 1024;
|
|
303
|
-
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
304
|
-
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
305
|
-
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
update(data) {
|
|
309
|
-
if (!data || !this.box) return;
|
|
310
|
-
|
|
311
|
-
const iface = data.interface || 'unknown';
|
|
312
|
-
const rx = data.rxSec || 0;
|
|
313
|
-
const tx = data.txSec || 0;
|
|
314
|
-
const total = rx + tx;
|
|
315
|
-
|
|
316
|
-
// Update history
|
|
317
|
-
this.history.push(total);
|
|
318
|
-
if (this.history.length > this.maxHistory) {
|
|
319
|
-
this.history.shift();
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
this.valueText.setContent(iface);
|
|
323
|
-
this.detailText.setContent(`↓${this.formatBytes(rx)} ↑${this.formatBytes(tx)}`);
|
|
324
|
-
|
|
325
|
-
if (this.sparkline && this.history.length > 1) {
|
|
326
|
-
this.sparkline.setData([this.history]);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
render(data) {
|
|
331
|
-
this.update(data);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
241
|
|
|
335
242
|
/**
|
|
336
243
|
* Disk Widget - Displays disk usage
|
|
@@ -1010,7 +917,6 @@ export const WIDGET_REGISTRY = {
|
|
|
1010
917
|
cpu: CpuWidget,
|
|
1011
918
|
memory: MemoryWidget,
|
|
1012
919
|
gpu: GpuWidget,
|
|
1013
|
-
network: NetworkWidget,
|
|
1014
920
|
disk: DiskWidget,
|
|
1015
921
|
system: SystemWidget,
|
|
1016
922
|
uptime: UptimeWidget,
|
|
@@ -1044,7 +950,6 @@ export default {
|
|
|
1044
950
|
CpuWidget,
|
|
1045
951
|
MemoryWidget,
|
|
1046
952
|
GpuWidget,
|
|
1047
|
-
NetworkWidget,
|
|
1048
953
|
DiskWidget,
|
|
1049
954
|
SystemWidget,
|
|
1050
955
|
UptimeWidget,
|
package/src/widgets/index.js
CHANGED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for removed dependencies and widgets
|
|
3
|
+
* Verifies cleanup after blessed-contrib and NetworkWidget removal
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { jest } from '@jest/globals';
|
|
7
|
+
import { describe, test, expect } from '@jest/globals';
|
|
8
|
+
import { readFileSync } from 'fs';
|
|
9
|
+
import { fileURLToPath } from 'url';
|
|
10
|
+
import { dirname, join } from 'path';
|
|
11
|
+
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = dirname(__filename);
|
|
14
|
+
|
|
15
|
+
describe('Removed dependencies cleanup', () => {
|
|
16
|
+
describe('blessed-contrib removal', () => {
|
|
17
|
+
test('should not have blessed-contrib in package.json dependencies', () => {
|
|
18
|
+
const pkg = JSON.parse(
|
|
19
|
+
readFileSync(join(__dirname, '../package.json'), 'utf8')
|
|
20
|
+
);
|
|
21
|
+
expect(pkg.dependencies).not.toHaveProperty('blessed-contrib');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('should not import blessed-contrib in index.js', () => {
|
|
25
|
+
const indexContent = readFileSync(join(__dirname, '../index.js'), 'utf8');
|
|
26
|
+
expect(indexContent).not.toContain("import contrib from 'blessed-contrib'");
|
|
27
|
+
expect(indexContent).not.toContain('blessed-contrib');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('NetworkWidget removal', () => {
|
|
32
|
+
test('should not export NetworkWidget from widgets/index.js', async () => {
|
|
33
|
+
const widgets = await import('../src/widgets/index.js');
|
|
34
|
+
expect(widgets.NetworkWidget).toBeUndefined();
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('should not have NetworkWidget in builtin-widgets exports', async () => {
|
|
38
|
+
const builtin = await import('../src/widgets/builtin-widgets.js');
|
|
39
|
+
expect(builtin.NetworkWidget).toBeUndefined();
|
|
40
|
+
expect(builtin.WIDGET_REGISTRY).not.toHaveProperty('network');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('builtin-widgets should not reference blessed-contrib', () => {
|
|
44
|
+
const content = readFileSync(
|
|
45
|
+
join(__dirname, '../src/widgets/builtin-widgets.js'),
|
|
46
|
+
'utf8'
|
|
47
|
+
);
|
|
48
|
+
expect(content).not.toContain('blessed-contrib');
|
|
49
|
+
expect(content).not.toContain('contrib.sparkline');
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('@pm2/blessed migration', () => {
|
|
54
|
+
test('should have @pm2/blessed as dependency', () => {
|
|
55
|
+
const pkg = JSON.parse(
|
|
56
|
+
readFileSync(join(__dirname, '../package.json'), 'utf8')
|
|
57
|
+
);
|
|
58
|
+
expect(pkg.dependencies).toHaveProperty('@pm2/blessed');
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('should have blessed aliased to @pm2/blessed', () => {
|
|
62
|
+
const pkg = JSON.parse(
|
|
63
|
+
readFileSync(join(__dirname, '../package.json'), 'utf8')
|
|
64
|
+
);
|
|
65
|
+
expect(pkg.dependencies.blessed).toBe('npm:@pm2/blessed@^0.1.81');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test('should be able to import blessed', async () => {
|
|
69
|
+
// This verifies the alias is working
|
|
70
|
+
const blessed = await import('blessed');
|
|
71
|
+
expect(typeof blessed.default).toBe('function');
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
});
|