claw-dashboard 2.0.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.
@@ -25,10 +25,10 @@ jobs:
25
25
 
26
26
  steps:
27
27
  - name: Checkout code
28
- uses: actions/checkout@v4
28
+ uses: actions/checkout@v6
29
29
 
30
30
  - name: Setup Node.js ${{ matrix.node-version }}
31
- uses: actions/setup-node@v4
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@v4
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@v4
66
+ uses: actions/checkout@v6
67
67
 
68
68
  - name: Setup Node.js
69
- uses: actions/setup-node@v4
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@v4
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@v4
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@v4
120
+ uses: actions/checkout@v6
121
121
 
122
122
  - name: Setup Node.js
123
- uses: actions/setup-node@v4
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@v3
144
+ uses: github/codeql-action/upload-sarif@v4
145
145
  if: always()
146
146
  with:
147
147
  sarif_file: 'trivy-results.sarif'
@@ -14,10 +14,10 @@ jobs:
14
14
 
15
15
  steps:
16
16
  - name: Checkout code
17
- uses: actions/checkout@v4
17
+ uses: actions/checkout@v6
18
18
 
19
19
  - name: Setup Node.js
20
- uses: actions/setup-node@v4
20
+ uses: actions/setup-node@v6
21
21
  with:
22
22
  node-version: 20
23
23
  cache: 'npm'
@@ -17,10 +17,10 @@ jobs:
17
17
 
18
18
  steps:
19
19
  - name: Checkout code
20
- uses: actions/checkout@v4
20
+ uses: actions/checkout@v6
21
21
 
22
22
  - name: Setup Node.js
23
- uses: actions/setup-node@v4
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@v4
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@v4
71
+ uses: actions/checkout@v6
72
72
 
73
73
  - name: Initialize CodeQL
74
- uses: github/codeql-action/init@v3
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@v3
79
+ uses: github/codeql-action/autobuild@v4
80
80
 
81
81
  - name: Perform CodeQL Analysis
82
- uses: github/codeql-action/analyze@v3
82
+ uses: github/codeql-action/analyze@v4
83
83
  with:
84
84
  category: "/language:${{matrix.language}}"
package/index.js CHANGED
@@ -397,6 +397,22 @@ async function getLatestVersion() {
397
397
  } catch { return null; }
398
398
  }
399
399
 
400
+ async function getLatestDashboardVersion() {
401
+ try {
402
+ return await new Promise((resolve) => {
403
+ https.get('https://api.github.com/repos/spleck/claw-dashboard/releases/latest', {
404
+ headers: { 'User-Agent': 'claw-dashboard' }
405
+ }, (res) => {
406
+ let data = '';
407
+ res.on('data', chunk => data += chunk);
408
+ res.on('end', () => {
409
+ try { resolve(JSON.parse(data).tag_name?.replace(/^v/, '')); } catch { resolve(null); }
410
+ });
411
+ }).on('error', () => resolve(null)).setTimeout(3000);
412
+ });
413
+ } catch { return null; }
414
+ }
415
+
400
416
  function formatDuration(seconds) {
401
417
  if (!seconds || seconds < 0) return '--';
402
418
  const days = Math.floor(seconds / 86400);
@@ -836,7 +852,7 @@ class Dashboard {
836
852
  // Favorites state
837
853
  this.showFavoritesOnly = this.settings.showFavoritesOnly || false;
838
854
  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: {} };
855
+ this.data = { cpu: [], memory: {}, openclaw: null, gpu: null, network: null, sessions: [], agents: [], version: null, latest: null, dashboardVersion: DASHBOARD_VERSION, dashboardLatest: null, sessionTPS: {}, sessionLastTPS: {} };
840
856
  // Data freshness tracking - stores timestamps when each data type was last successfully fetched
841
857
  this.dataTimestamps = { cpu: null, memory: null, gpu: null, network: null, disk: null, system: null, sessions: null };
842
858
  this.prev = null;
@@ -1265,6 +1281,13 @@ class Dashboard {
1265
1281
  }).catch(() => {
1266
1282
  this.data.latest = null;
1267
1283
  });
1284
+
1285
+ // Also check for dashboard updates
1286
+ getLatestDashboardVersion().then(latest => {
1287
+ this.data.dashboardLatest = latest;
1288
+ }).catch(() => {
1289
+ this.data.dashboardLatest = null;
1290
+ });
1268
1291
  }
1269
1292
 
1270
1293
  createWidgets() {
@@ -5081,11 +5104,23 @@ class Dashboard {
5081
5104
  }
5082
5105
  }
5083
5106
 
5107
+ // Fetch gateway uptime FIRST - if process isn't running, gateway is down
5108
+ // This must be checked before fetchSessions() since it can succeed via local fallback
5109
+ this.data.gatewayUptime = await getGatewayUptime();
5110
+ const isGatewayProcessRunning = this.data.gatewayUptime !== null;
5111
+
5084
5112
  // Fetch sessions via API (same as clawps) - has displayName and channel
5085
5113
  try {
5086
5114
  const sessions = await timeOperation('Sessions', () => this.fetchSessions(), 5000);
5087
5115
  this.data.sessions = sessions || [];
5088
- this.data.openclaw = { gateway: { reachable: true } };
5116
+ // Gateway is ONLY reachable if: process is running AND at least one endpoint responds
5117
+ const stats = this.data.gatewayStats;
5118
+ const allUnreachable = stats && stats.totalEndpoints > 0 && stats.reachableEndpoints === 0;
5119
+ if (!isGatewayProcessRunning || allUnreachable) {
5120
+ this.data.openclaw = { gateway: { reachable: false } };
5121
+ } else {
5122
+ this.data.openclaw = { gateway: { reachable: true } };
5123
+ }
5089
5124
  this.dataTimestamps.sessions = now;
5090
5125
 
5091
5126
  // Clean up stale sessionTPS entries for sessions that no longer exist
@@ -5437,7 +5472,18 @@ class Dashboard {
5437
5472
  openclawText = `openclaw ${current}`;
5438
5473
  }
5439
5474
  }
5440
- this.diffRenderer.setContent('title', this.w.title, `Dashboard ${DASHBOARD_VERSION}, ${openclawText}`);
5475
+
5476
+ // Render dashboard version with update check
5477
+ let dashboardText = `dashboard ${this.data.dashboardVersion}`;
5478
+ if (this.data.dashboardLatest && this.data.dashboardVersion) {
5479
+ if (this.data.dashboardVersion === this.data.dashboardLatest) {
5480
+ dashboardText = `dashboard ${this.data.dashboardVersion} ✓`;
5481
+ } else {
5482
+ dashboardText = `dashboard ${this.data.dashboardVersion} → ${this.data.dashboardLatest}`;
5483
+ }
5484
+ }
5485
+
5486
+ this.diffRenderer.setContent('title', this.w.title, `Claw ${dashboardText}, ${openclawText}`);
5441
5487
 
5442
5488
  // Update clock - show current local time, PAUSED indicator on the right (with differential updates)
5443
5489
  const now = new Date();
@@ -5588,9 +5634,12 @@ class Dashboard {
5588
5634
  const gatewayHealth = gatewayManager.getEndpointHealth();
5589
5635
  const unreachableCount = gatewayHealth.filter(ep => ep.enabled && !ep.reachable).length;
5590
5636
  const enabledCount = gatewayHealth.filter(ep => ep.enabled).length;
5637
+ const isGatewayProcessRunning = this.data.gatewayUptime !== null;
5591
5638
  let gatewayIndicator = '';
5592
5639
  if (enabledCount > 0) {
5593
- if (unreachableCount === 0) {
5640
+ if (!isGatewayProcessRunning) {
5641
+ gatewayIndicator = '{red-fg}✗ gateway offline{/red-fg} [G] retry ';
5642
+ } else if (unreachableCount === 0) {
5594
5643
  gatewayIndicator = '{green-fg}● gateway{/green-fg} ';
5595
5644
  } else if (unreachableCount === enabledCount) {
5596
5645
  gatewayIndicator = '{red-fg}✗ gateway offline{/red-fg} [G] retry ';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claw-dashboard",
3
- "version": "2.0.0",
3
+ "version": "2.1.0",
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,7 @@
60
60
  "node": ">=18"
61
61
  },
62
62
  "dependencies": {
63
- "blessed": "^0.1.81",
64
- "blessed-contrib": "^1.0.11",
63
+ "@pm2/blessed": "^0.1.81",
65
64
  "chalk": "^5.3.0",
66
65
  "sql.js": "^1.14.0",
67
66
  "systeminformation": "^5.21.22"
@@ -69,7 +68,7 @@
69
68
  "devDependencies": {
70
69
  "@eslint/js": "^10.0.1",
71
70
  "c8": "^11.0.0",
72
- "esbuild": "^0.25.12",
71
+ "esbuild": "^0.27.3",
73
72
  "eslint": "^10.0.2",
74
73
  "husky": "^9.1.7",
75
74
  "jest": "^30.2.0",
@@ -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 blessed-contrib',
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 blessed-contrib
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 blessed-contrib line chart
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 the line chart using blessed-contrib
672
- this.chart = contrib.default.line({
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: 14,
678
- style: {
679
- line: C.green || 'green',
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
- // Prepare chart data
752
- const chartData = {
753
- title: '${className}',
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 based on type
1123
- if (this.gaugeType === 'circle') {
1124
- this.gauge = contrib.default.gauge({
1125
- parent: this.box,
1126
- top: 1,
1127
- left: 'center',
1128
- width: '90%',
1129
- height: 6,
1130
- style: {
1131
- label: { fg: C.white || 'white' },
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.gauge || !result) return;
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.gauge) {
1246
- this.gauge.destroy();
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
  /**