claw-dashboard 2.1.1 → 2.2.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/CHANGELOG.md +29 -1
- package/docs/API.md +1 -2
- package/index.js +31 -41
- package/package.json +22 -12
- package/src/auto-save.js +11 -5
- package/src/cli/export-snapshot.js +3 -1
- package/src/cli/import-snapshot.js +3 -1
- package/src/config.js +9 -15
- package/src/errors.js +0 -9
- package/src/security.js +6 -2
- package/src/snapshot.js +73 -26
- package/src/web-server.js +7 -10
- package/.c8rc.json +0 -38
- package/.dockerignore +0 -68
- package/.github/dependabot.yml +0 -45
- package/.github/pull_request_template.md +0 -39
- package/.github/workflows/ci.yml +0 -147
- package/.github/workflows/release.yml +0 -40
- package/.github/workflows/security.yml +0 -84
- package/.husky/pre-commit +0 -1
- package/.planning/codebase/ARCHITECTURE.md +0 -206
- package/.planning/codebase/INTEGRATIONS.md +0 -150
- package/.planning/codebase/STACK.md +0 -122
- package/.planning/codebase/STRUCTURE.md +0 -201
- package/CONTRIBUTING.md +0 -378
- package/Dockerfile +0 -54
- package/FEATURES.md +0 -307
- package/TODO.md +0 -28
- package/ai.openclaw.dashboard.plist +0 -35
- package/build-cjs.js +0 -127
- package/cjs-shim.js +0 -13
- package/dist/clawdash +0 -1729
- package/dist/clawdash.meta.json +0 -2236
- package/dist/widgets.cjs +0 -6511
- package/docker-compose.yml +0 -67
- package/esbuild.config.js +0 -158
- package/eslint.config.js +0 -56
- package/examples/plugins/README.md +0 -122
- package/examples/plugins/api-status/index.js +0 -294
- package/examples/plugins/api-status/plugin.json +0 -19
- package/examples/plugins/hello-world/index.js +0 -104
- package/examples/plugins/hello-world/plugin.json +0 -15
- package/examples/plugins/system-metrics-chart/index.js +0 -339
- package/examples/plugins/system-metrics-chart/plugin.json +0 -18
- package/examples/plugins/weather-widget/index.js +0 -136
- package/examples/plugins/weather-widget/plugin.json +0 -19
- package/index.cjs +0 -23005
- package/jest.config.js +0 -11
- package/scripts/release.js +0 -595
- package/src/database.js +0 -734
- package/tests/alerts.test.js +0 -437
- package/tests/auto-save.test.js +0 -529
- package/tests/cache.test.js +0 -317
- package/tests/cli.test.js +0 -351
- package/tests/command-palette.test.js +0 -320
- package/tests/config-processor.test.js +0 -452
- package/tests/config-validator.test.js +0 -710
- package/tests/config-watcher.test.js +0 -594
- package/tests/config.test.js +0 -755
- package/tests/database.test.js +0 -438
- package/tests/errors.test.js +0 -189
- package/tests/example-plugins.test.js +0 -624
- package/tests/integration.test.js +0 -1321
- package/tests/loading-states.test.js +0 -300
- package/tests/manifest-validation-on-load.test.js +0 -671
- package/tests/performance-monitor.test.js +0 -190
- package/tests/plugin-api-rate-limit.test.js +0 -302
- package/tests/plugin-errors.test.js +0 -311
- package/tests/plugin-lifecycle-e2e.test.js +0 -1036
- package/tests/plugin-reload.test.js +0 -764
- package/tests/rate-limiter.test.js +0 -539
- package/tests/removed-dependencies.test.js +0 -74
- package/tests/retry.test.js +0 -308
- package/tests/security.test.js +0 -411
- package/tests/settings-modal.test.js +0 -226
- package/tests/theme-selector.test.js +0 -57
- package/tests/utils.js +0 -242
- package/tests/utils.test.js +0 -317
- package/tests/validate-plugin-cli.test.js +0 -359
- package/tests/validation.test.js +0 -837
- package/tests/web-server.test.js +0 -646
- package/tests/widget-arrange-mode.test.js +0 -183
- package/tests/widget-config-hot-reload.test.js +0 -465
- package/tests/widget-dependency.test.js +0 -591
- package/tests/widget-error-boundary.test.js +0 -749
- package/tests/widget-error-isolation.test.js +0 -469
- package/tests/widget-integration.test.js +0 -1147
- package/tests/widget-refresh-intervals.test.js +0 -284
- package/tests/worker-pool.test.js +0 -522
package/src/database.js
DELETED
|
@@ -1,734 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SQLite database module for historical data persistence
|
|
3
|
-
* Uses sql.js (WebAssembly-based SQLite) for cross-platform compatibility
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import initSqlJs from 'sql.js';
|
|
7
|
-
import fs from 'fs';
|
|
8
|
-
import os from 'os';
|
|
9
|
-
import path from 'path';
|
|
10
|
-
import { fileURLToPath } from 'url';
|
|
11
|
-
import logger from './logger.js';
|
|
12
|
-
import config from './config.js';
|
|
13
|
-
|
|
14
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
15
|
-
const __dirname = path.dirname(__filename);
|
|
16
|
-
|
|
17
|
-
// Database file path
|
|
18
|
-
const DB_PATH = config.DATABASE.PATH;
|
|
19
|
-
|
|
20
|
-
// Database instance
|
|
21
|
-
let db = null;
|
|
22
|
-
let SQL = null;
|
|
23
|
-
let saveInterval = null;
|
|
24
|
-
let cleanupInterval = null;
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Initialize the database and create tables if needed
|
|
28
|
-
*/
|
|
29
|
-
export async function initDatabase() {
|
|
30
|
-
try {
|
|
31
|
-
// Initialize SQL.js
|
|
32
|
-
SQL = await initSqlJs();
|
|
33
|
-
|
|
34
|
-
// Load existing database or create new one
|
|
35
|
-
let data = null;
|
|
36
|
-
try {
|
|
37
|
-
if (fs.existsSync(DB_PATH)) {
|
|
38
|
-
data = fs.readFileSync(DB_PATH);
|
|
39
|
-
logger.info('Loaded existing database from ' + DB_PATH);
|
|
40
|
-
}
|
|
41
|
-
} catch (err) {
|
|
42
|
-
logger.warn('Could not load existing database: ' + err.message);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
db = new SQL.Database(data);
|
|
46
|
-
|
|
47
|
-
// Create tables
|
|
48
|
-
createTables();
|
|
49
|
-
|
|
50
|
-
// Set up periodic save (unref allows Jest to exit cleanly)
|
|
51
|
-
saveInterval = setInterval(saveDatabase, config.DATABASE.SAVE_INTERVAL_MS).unref();
|
|
52
|
-
cleanupInterval = setInterval(cleanupOldData, config.DATABASE.CLEANUP_INTERVAL_MS).unref();
|
|
53
|
-
|
|
54
|
-
logger.info('Database initialized successfully');
|
|
55
|
-
return true;
|
|
56
|
-
} catch (err) {
|
|
57
|
-
logger.error('Failed to initialize database: ' + err.message);
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Create all required tables
|
|
64
|
-
*/
|
|
65
|
-
function createTables() {
|
|
66
|
-
// Session snapshots table
|
|
67
|
-
db.run(`
|
|
68
|
-
CREATE TABLE IF NOT EXISTS session_snapshots (
|
|
69
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
70
|
-
session_id TEXT NOT NULL,
|
|
71
|
-
agent TEXT,
|
|
72
|
-
channel TEXT,
|
|
73
|
-
model TEXT,
|
|
74
|
-
tokens INTEGER DEFAULT 0,
|
|
75
|
-
status TEXT,
|
|
76
|
-
created_at INTEGER NOT NULL,
|
|
77
|
-
updated_at INTEGER NOT NULL
|
|
78
|
-
)
|
|
79
|
-
`);
|
|
80
|
-
|
|
81
|
-
// Create index for faster queries
|
|
82
|
-
db.run(`
|
|
83
|
-
CREATE INDEX IF NOT EXISTS idx_session_snapshots_created
|
|
84
|
-
ON session_snapshots(created_at)
|
|
85
|
-
`);
|
|
86
|
-
|
|
87
|
-
db.run(`
|
|
88
|
-
CREATE INDEX IF NOT EXISTS idx_session_snapshots_session_id
|
|
89
|
-
ON session_snapshots(session_id)
|
|
90
|
-
`);
|
|
91
|
-
|
|
92
|
-
// CPU metrics history table
|
|
93
|
-
db.run(`
|
|
94
|
-
CREATE TABLE IF NOT EXISTS cpu_metrics (
|
|
95
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
96
|
-
timestamp INTEGER NOT NULL,
|
|
97
|
-
cpu_count INTEGER,
|
|
98
|
-
load_avg_1 REAL,
|
|
99
|
-
load_avg_5 REAL,
|
|
100
|
-
load_avg_15 REAL,
|
|
101
|
-
cpu_usage_user REAL,
|
|
102
|
-
cpu_usage_system REAL,
|
|
103
|
-
cpu_usage_idle REAL,
|
|
104
|
-
cpu_usage_irq REAL,
|
|
105
|
-
cpu_usage_soft_irq REAL,
|
|
106
|
-
cpu_usage_stolen REAL
|
|
107
|
-
)
|
|
108
|
-
`);
|
|
109
|
-
|
|
110
|
-
db.run(`
|
|
111
|
-
CREATE INDEX IF NOT EXISTS idx_cpu_metrics_timestamp
|
|
112
|
-
ON cpu_metrics(timestamp)
|
|
113
|
-
`);
|
|
114
|
-
|
|
115
|
-
// Memory metrics history table
|
|
116
|
-
db.run(`
|
|
117
|
-
CREATE TABLE IF NOT EXISTS memory_metrics (
|
|
118
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
119
|
-
timestamp INTEGER NOT NULL,
|
|
120
|
-
total_bytes INTEGER,
|
|
121
|
-
used_bytes INTEGER,
|
|
122
|
-
free_bytes INTEGER,
|
|
123
|
-
available_bytes INTEGER,
|
|
124
|
-
used_percent REAL,
|
|
125
|
-
swap_total_bytes INTEGER,
|
|
126
|
-
swap_used_bytes INTEGER,
|
|
127
|
-
swap_free_bytes INTEGER,
|
|
128
|
-
swap_used_percent REAL
|
|
129
|
-
)
|
|
130
|
-
`);
|
|
131
|
-
|
|
132
|
-
db.run(`
|
|
133
|
-
CREATE INDEX IF NOT EXISTS idx_memory_metrics_timestamp
|
|
134
|
-
ON memory_metrics(timestamp)
|
|
135
|
-
`);
|
|
136
|
-
|
|
137
|
-
// Network metrics history table
|
|
138
|
-
db.run(`
|
|
139
|
-
CREATE TABLE IF NOT EXISTS network_metrics (
|
|
140
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
141
|
-
timestamp INTEGER NOT NULL,
|
|
142
|
-
interface_name TEXT,
|
|
143
|
-
rx_bytes INTEGER,
|
|
144
|
-
tx_bytes INTEGER,
|
|
145
|
-
rx_sec REAL,
|
|
146
|
-
tx_sec REAL,
|
|
147
|
-
ms REAL
|
|
148
|
-
)
|
|
149
|
-
`);
|
|
150
|
-
|
|
151
|
-
db.run(`
|
|
152
|
-
CREATE INDEX IF NOT EXISTS idx_network_metrics_timestamp
|
|
153
|
-
ON network_metrics(timestamp)
|
|
154
|
-
`);
|
|
155
|
-
|
|
156
|
-
logger.debug('Database tables created');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* Save database to disk
|
|
161
|
-
*/
|
|
162
|
-
function saveDatabase() {
|
|
163
|
-
if (!db) return;
|
|
164
|
-
|
|
165
|
-
try {
|
|
166
|
-
const data = db.export();
|
|
167
|
-
const buffer = Buffer.from(data);
|
|
168
|
-
|
|
169
|
-
// Ensure directory exists
|
|
170
|
-
const dir = path.dirname(DB_PATH);
|
|
171
|
-
if (!fs.existsSync(dir)) {
|
|
172
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
fs.writeFileSync(DB_PATH, buffer);
|
|
176
|
-
logger.debug('Database saved to disk');
|
|
177
|
-
} catch (err) {
|
|
178
|
-
logger.error('Failed to save database: ' + err.message);
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
/**
|
|
183
|
-
* Store a session snapshot
|
|
184
|
-
* @param {Object} session - Session object from openclaw
|
|
185
|
-
*/
|
|
186
|
-
export function storeSessionSnapshot(session) {
|
|
187
|
-
if (!db || !session) return;
|
|
188
|
-
|
|
189
|
-
try {
|
|
190
|
-
const now = Date.now();
|
|
191
|
-
|
|
192
|
-
// Extract relevant fields from session
|
|
193
|
-
const sessionId = session.sessionId || session.key || null;
|
|
194
|
-
const agent = session.agent || extractAgent(session.key) || 'unknown';
|
|
195
|
-
const channel = session.deliveryContext?.channel ||
|
|
196
|
-
session.origin?.channel ||
|
|
197
|
-
session.origin?.surface || 'unknown';
|
|
198
|
-
const model = session.model || session.llmModel || 'unknown';
|
|
199
|
-
const tokens = session.totalTokens || session.tokens || 0;
|
|
200
|
-
const status = session.status || (session.systemRunning ? 'running' : 'idle');
|
|
201
|
-
|
|
202
|
-
// Check if session already exists, update or insert
|
|
203
|
-
const existing = db.exec(
|
|
204
|
-
'SELECT id FROM session_snapshots WHERE session_id = ? ORDER BY created_at DESC LIMIT 1',
|
|
205
|
-
[sessionId]
|
|
206
|
-
);
|
|
207
|
-
|
|
208
|
-
if (existing.length > 0 && existing[0].values.length > 0) {
|
|
209
|
-
// Update existing
|
|
210
|
-
db.run(
|
|
211
|
-
`UPDATE session_snapshots
|
|
212
|
-
SET agent = ?, channel = ?, model = ?, tokens = ?, status = ?, updated_at = ?
|
|
213
|
-
WHERE session_id = ?`,
|
|
214
|
-
[agent, channel, model, tokens, status, now, sessionId]
|
|
215
|
-
);
|
|
216
|
-
} else {
|
|
217
|
-
// Insert new
|
|
218
|
-
db.run(
|
|
219
|
-
`INSERT INTO session_snapshots (session_id, agent, channel, model, tokens, status, created_at, updated_at)
|
|
220
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
221
|
-
[sessionId, agent, channel, model, tokens, status, now, now]
|
|
222
|
-
);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
logger.debug('Stored session snapshot: ' + sessionId);
|
|
226
|
-
} catch (err) {
|
|
227
|
-
logger.error('Failed to store session snapshot: ' + err.message);
|
|
228
|
-
}
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Store CPU metrics
|
|
233
|
-
* @param {Object} cpuData - CPU metrics data
|
|
234
|
-
*/
|
|
235
|
-
export function storeCpuMetrics(cpuData) {
|
|
236
|
-
if (!db || !cpuData) return;
|
|
237
|
-
|
|
238
|
-
try {
|
|
239
|
-
const now = Date.now();
|
|
240
|
-
|
|
241
|
-
// Handle both array of CPUs and single CPU object
|
|
242
|
-
const cpus = cpuData.cpus || [cpuData] || [];
|
|
243
|
-
|
|
244
|
-
for (const cpu of cpus) {
|
|
245
|
-
db.run(
|
|
246
|
-
`INSERT INTO cpu_metrics (
|
|
247
|
-
timestamp, cpu_count, load_avg_1, load_avg_5, load_avg_15,
|
|
248
|
-
cpu_usage_user, cpu_usage_system, cpu_usage_idle,
|
|
249
|
-
cpu_usage_irq, cpu_usage_soft_irq, cpu_usage_stolen
|
|
250
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
251
|
-
[
|
|
252
|
-
now,
|
|
253
|
-
cpu.cpuCount || cpu.cpu_count || cpus.length || 1,
|
|
254
|
-
cpu.loadAvg1 || cpu.load_avg_1 || cpu.loadavg?.[0] || 0,
|
|
255
|
-
cpu.loadAvg5 || cpu.load_avg_5 || cpu.loadavg?.[1] || 0,
|
|
256
|
-
cpu.loadAvg15 || cpu.load_avg_15 || cpu.loadavg?.[2] || 0,
|
|
257
|
-
cpu.cpuUsageUser || cpu.cpu_usage_user || cpu.cpuUsage?.[0] || 0,
|
|
258
|
-
cpu.cpuUsageSystem || cpu.cpu_usage_system || cpu.cpuUsage?.[1] || 0,
|
|
259
|
-
cpu.cpuUsageIdle || cpu.cpu_usage_idle || cpu.cpuUsage?.[2] || 100,
|
|
260
|
-
cpu.cpuUsageIrq || cpu.cpu_usage_irq || cpu.cpuUsage?.[3] || 0,
|
|
261
|
-
cpu.cpuUsageSoftIrq || cpu.cpu_usage_soft_irq || cpu.cpuUsage?.[4] || 0,
|
|
262
|
-
cpu.cpuUsageStolen || cpu.cpu_usage_stolen || cpu.cpuUsage?.[5] || 0
|
|
263
|
-
]
|
|
264
|
-
);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
logger.debug('Stored CPU metrics');
|
|
268
|
-
} catch (err) {
|
|
269
|
-
logger.error('Failed to store CPU metrics: ' + err.message);
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Store memory metrics
|
|
275
|
-
* @param {Object} memoryData - Memory metrics data
|
|
276
|
-
*/
|
|
277
|
-
export function storeMemoryMetrics(memoryData) {
|
|
278
|
-
if (!db || !memoryData) return;
|
|
279
|
-
|
|
280
|
-
try {
|
|
281
|
-
const now = Date.now();
|
|
282
|
-
|
|
283
|
-
db.run(
|
|
284
|
-
`INSERT INTO memory_metrics (
|
|
285
|
-
timestamp, total_bytes, used_bytes, free_bytes, available_bytes,
|
|
286
|
-
used_percent, swap_total_bytes, swap_used_bytes, swap_free_bytes, swap_used_percent
|
|
287
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
288
|
-
[
|
|
289
|
-
now,
|
|
290
|
-
memoryData.totalBytes || memoryData.total_bytes || 0,
|
|
291
|
-
memoryData.usedBytes || memoryData.used_bytes || 0,
|
|
292
|
-
memoryData.freeBytes || memoryData.free_bytes || 0,
|
|
293
|
-
memoryData.availableBytes || memoryData.available_bytes || 0,
|
|
294
|
-
memoryData.usedPercent || memoryData.used_percent || 0,
|
|
295
|
-
memoryData.swapTotalBytes || memoryData.swap_total_bytes || 0,
|
|
296
|
-
memoryData.swapUsedBytes || memoryData.swap_used_bytes || 0,
|
|
297
|
-
memoryData.swapFreeBytes || memoryData.swap_free_bytes || 0,
|
|
298
|
-
memoryData.swapUsedPercent || memoryData.swap_used_percent || 0
|
|
299
|
-
]
|
|
300
|
-
);
|
|
301
|
-
|
|
302
|
-
logger.debug('Stored memory metrics');
|
|
303
|
-
} catch (err) {
|
|
304
|
-
logger.error('Failed to store memory metrics: ' + err.message);
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* Store network metrics
|
|
310
|
-
* @param {Object} networkData - Network metrics data
|
|
311
|
-
*/
|
|
312
|
-
export function storeNetworkMetrics(networkData) {
|
|
313
|
-
if (!db || !networkData) return;
|
|
314
|
-
|
|
315
|
-
try {
|
|
316
|
-
const now = Date.now();
|
|
317
|
-
|
|
318
|
-
// Handle both array and single interface
|
|
319
|
-
const interfaces = Array.isArray(networkData) ? networkData : [networkData];
|
|
320
|
-
|
|
321
|
-
for (const iface of interfaces) {
|
|
322
|
-
db.run(
|
|
323
|
-
`INSERT INTO network_metrics (
|
|
324
|
-
timestamp, interface_name, rx_bytes, tx_bytes, rx_sec, tx_sec, ms
|
|
325
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?)`,
|
|
326
|
-
[
|
|
327
|
-
now,
|
|
328
|
-
iface.interfaceName || iface.interface_name || 'unknown',
|
|
329
|
-
iface.rxBytes || iface.rx_bytes || 0,
|
|
330
|
-
iface.txBytes || iface.tx_bytes || 0,
|
|
331
|
-
iface.rxSec || iface.rx_sec || 0,
|
|
332
|
-
iface.txSec || iface.tx_sec || 0,
|
|
333
|
-
iface.ms || 0
|
|
334
|
-
]
|
|
335
|
-
);
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
logger.debug('Stored network metrics');
|
|
339
|
-
} catch (err) {
|
|
340
|
-
logger.error('Failed to store network metrics: ' + err.message);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
/**
|
|
345
|
-
* Get sessions from the last 24 hours
|
|
346
|
-
* @returns {Array} Session snapshots
|
|
347
|
-
*/
|
|
348
|
-
export function getSessionsLast24Hours() {
|
|
349
|
-
return getSessionsByHours(24);
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
/**
|
|
353
|
-
* Get sessions from the last 7 days
|
|
354
|
-
* @returns {Array} Session snapshots
|
|
355
|
-
*/
|
|
356
|
-
export function getSessionsLast7Days() {
|
|
357
|
-
return getSessionsByDays(7);
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
/**
|
|
361
|
-
* Get sessions by number of hours
|
|
362
|
-
* @param {number} hours - Hours to look back
|
|
363
|
-
* @returns {Array} Session snapshots
|
|
364
|
-
*/
|
|
365
|
-
export function getSessionsByHours(hours = 24) {
|
|
366
|
-
if (!db) return [];
|
|
367
|
-
|
|
368
|
-
try {
|
|
369
|
-
const cutoff = Date.now() - (hours * 60 * 60 * 1000);
|
|
370
|
-
|
|
371
|
-
const result = db.exec(
|
|
372
|
-
`SELECT session_id, agent, channel, model, tokens, status, created_at, updated_at
|
|
373
|
-
FROM session_snapshots
|
|
374
|
-
WHERE created_at >= ?
|
|
375
|
-
ORDER BY created_at DESC`,
|
|
376
|
-
[cutoff]
|
|
377
|
-
);
|
|
378
|
-
|
|
379
|
-
if (result.length === 0) return [];
|
|
380
|
-
|
|
381
|
-
return result[0].values.map(row => ({
|
|
382
|
-
sessionId: row[0],
|
|
383
|
-
agent: row[1],
|
|
384
|
-
channel: row[2],
|
|
385
|
-
model: row[3],
|
|
386
|
-
tokens: row[4],
|
|
387
|
-
status: row[5],
|
|
388
|
-
createdAt: row[6],
|
|
389
|
-
updatedAt: row[7]
|
|
390
|
-
}));
|
|
391
|
-
} catch (err) {
|
|
392
|
-
logger.error('Failed to get sessions by hours: ' + err.message);
|
|
393
|
-
return [];
|
|
394
|
-
}
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
/**
|
|
398
|
-
* Get sessions by number of days
|
|
399
|
-
* @param {number} days - Days to look back
|
|
400
|
-
* @returns {Array} Session snapshots
|
|
401
|
-
*/
|
|
402
|
-
export function getSessionsByDays(days = 7) {
|
|
403
|
-
if (!db) return [];
|
|
404
|
-
|
|
405
|
-
try {
|
|
406
|
-
const cutoff = Date.now() - (days * 24 * 60 * 60 * 1000);
|
|
407
|
-
|
|
408
|
-
const result = db.exec(
|
|
409
|
-
`SELECT session_id, agent, channel, model, tokens, status, created_at, updated_at
|
|
410
|
-
FROM session_snapshots
|
|
411
|
-
WHERE created_at >= ?
|
|
412
|
-
ORDER BY created_at DESC`,
|
|
413
|
-
[cutoff]
|
|
414
|
-
);
|
|
415
|
-
|
|
416
|
-
if (result.length === 0) return [];
|
|
417
|
-
|
|
418
|
-
return result[0].values.map(row => ({
|
|
419
|
-
sessionId: row[0],
|
|
420
|
-
agent: row[1],
|
|
421
|
-
channel: row[2],
|
|
422
|
-
model: row[3],
|
|
423
|
-
tokens: row[4],
|
|
424
|
-
status: row[5],
|
|
425
|
-
createdAt: row[6],
|
|
426
|
-
updatedAt: row[7]
|
|
427
|
-
}));
|
|
428
|
-
} catch (err) {
|
|
429
|
-
logger.error('Failed to get sessions by days: ' + err.message);
|
|
430
|
-
return [];
|
|
431
|
-
}
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
/**
|
|
435
|
-
* Get CPU metrics history for the last N hours
|
|
436
|
-
* @param {number} hours - Hours to look back
|
|
437
|
-
* @returns {Array} CPU metrics
|
|
438
|
-
*/
|
|
439
|
-
export function getCpuMetricsHistory(hours = 24) {
|
|
440
|
-
if (!db) return [];
|
|
441
|
-
|
|
442
|
-
try {
|
|
443
|
-
const cutoff = Date.now() - (hours * 60 * 60 * 1000);
|
|
444
|
-
|
|
445
|
-
const result = db.exec(
|
|
446
|
-
`SELECT timestamp, cpu_count, load_avg_1, load_avg_5, load_avg_15,
|
|
447
|
-
cpu_usage_user, cpu_usage_system, cpu_usage_idle
|
|
448
|
-
FROM cpu_metrics
|
|
449
|
-
WHERE timestamp >= ?
|
|
450
|
-
ORDER BY timestamp ASC`,
|
|
451
|
-
[cutoff]
|
|
452
|
-
);
|
|
453
|
-
|
|
454
|
-
if (result.length === 0) return [];
|
|
455
|
-
|
|
456
|
-
return result[0].values.map(row => ({
|
|
457
|
-
timestamp: row[0],
|
|
458
|
-
cpuCount: row[1],
|
|
459
|
-
loadAvg1: row[2],
|
|
460
|
-
loadAvg5: row[3],
|
|
461
|
-
loadAvg15: row[4],
|
|
462
|
-
cpuUsageUser: row[5],
|
|
463
|
-
cpuUsageSystem: row[6],
|
|
464
|
-
cpuUsageIdle: row[7]
|
|
465
|
-
}));
|
|
466
|
-
} catch (err) {
|
|
467
|
-
logger.error('Failed to get CPU metrics history: ' + err.message);
|
|
468
|
-
return [];
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
/**
|
|
473
|
-
* Get memory metrics history for the last N hours
|
|
474
|
-
* @param {number} hours - Hours to look back
|
|
475
|
-
* @returns {Array} Memory metrics
|
|
476
|
-
*/
|
|
477
|
-
export function getMemoryMetricsHistory(hours = 24) {
|
|
478
|
-
if (!db) return [];
|
|
479
|
-
|
|
480
|
-
try {
|
|
481
|
-
const cutoff = Date.now() - (hours * 60 * 60 * 1000);
|
|
482
|
-
|
|
483
|
-
const result = db.exec(
|
|
484
|
-
`SELECT timestamp, total_bytes, used_bytes, free_bytes, available_bytes, used_percent
|
|
485
|
-
FROM memory_metrics
|
|
486
|
-
WHERE timestamp >= ?
|
|
487
|
-
ORDER BY timestamp ASC`,
|
|
488
|
-
[cutoff]
|
|
489
|
-
);
|
|
490
|
-
|
|
491
|
-
if (result.length === 0) return [];
|
|
492
|
-
|
|
493
|
-
return result[0].values.map(row => ({
|
|
494
|
-
timestamp: row[0],
|
|
495
|
-
totalBytes: row[1],
|
|
496
|
-
usedBytes: row[2],
|
|
497
|
-
freeBytes: row[3],
|
|
498
|
-
availableBytes: row[4],
|
|
499
|
-
usedPercent: row[5]
|
|
500
|
-
}));
|
|
501
|
-
} catch (err) {
|
|
502
|
-
logger.error('Failed to get memory metrics history: ' + err.message);
|
|
503
|
-
return [];
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
/**
|
|
508
|
-
* Get network metrics history for the last N hours
|
|
509
|
-
* @param {number} hours - Hours to look back
|
|
510
|
-
* @returns {Array} Network metrics
|
|
511
|
-
*/
|
|
512
|
-
export function getNetworkMetricsHistory(hours = 24) {
|
|
513
|
-
if (!db) return [];
|
|
514
|
-
|
|
515
|
-
try {
|
|
516
|
-
const cutoff = Date.now() - (hours * 60 * 60 * 1000);
|
|
517
|
-
|
|
518
|
-
const result = db.exec(
|
|
519
|
-
`SELECT timestamp, interface_name, rx_bytes, tx_bytes, rx_sec, tx_sec
|
|
520
|
-
FROM network_metrics
|
|
521
|
-
WHERE timestamp >= ?
|
|
522
|
-
ORDER BY timestamp ASC`,
|
|
523
|
-
[cutoff]
|
|
524
|
-
);
|
|
525
|
-
|
|
526
|
-
if (result.length === 0) return [];
|
|
527
|
-
|
|
528
|
-
return result[0].values.map(row => ({
|
|
529
|
-
timestamp: row[0],
|
|
530
|
-
interfaceName: row[1],
|
|
531
|
-
rxBytes: row[2],
|
|
532
|
-
txBytes: row[3],
|
|
533
|
-
rxSec: row[4],
|
|
534
|
-
txSec: row[5]
|
|
535
|
-
}));
|
|
536
|
-
} catch (err) {
|
|
537
|
-
logger.error('Failed to get network metrics history: ' + err.message);
|
|
538
|
-
return [];
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
/**
|
|
543
|
-
* Get aggregated metrics summary
|
|
544
|
-
* @param {number} hours - Hours to look back
|
|
545
|
-
* @returns {Object} Aggregated metrics
|
|
546
|
-
*/
|
|
547
|
-
export function getMetricsSummary(hours = 24) {
|
|
548
|
-
if (!db) return null;
|
|
549
|
-
|
|
550
|
-
try {
|
|
551
|
-
const cutoff = Date.now() - (hours * 60 * 60 * 1000);
|
|
552
|
-
|
|
553
|
-
// Get CPU summary
|
|
554
|
-
const cpuResult = db.exec(
|
|
555
|
-
`SELECT
|
|
556
|
-
AVG(cpu_usage_user) as avg_user,
|
|
557
|
-
MAX(cpu_usage_user) as max_user,
|
|
558
|
-
AVG(load_avg_1) as avg_load
|
|
559
|
-
FROM cpu_metrics
|
|
560
|
-
WHERE timestamp >= ?`,
|
|
561
|
-
[cutoff]
|
|
562
|
-
);
|
|
563
|
-
|
|
564
|
-
// Get Memory summary
|
|
565
|
-
const memResult = db.exec(
|
|
566
|
-
`SELECT
|
|
567
|
-
AVG(used_percent) as avg_used,
|
|
568
|
-
MAX(used_percent) as max_used,
|
|
569
|
-
AVG(available_bytes) as avg_available
|
|
570
|
-
FROM memory_metrics
|
|
571
|
-
WHERE timestamp >= ?`,
|
|
572
|
-
[cutoff]
|
|
573
|
-
);
|
|
574
|
-
|
|
575
|
-
// Get session count
|
|
576
|
-
const sessionResult = db.exec(
|
|
577
|
-
`SELECT COUNT(DISTINCT session_id) as session_count
|
|
578
|
-
FROM session_snapshots
|
|
579
|
-
WHERE created_at >= ?`,
|
|
580
|
-
[cutoff]
|
|
581
|
-
);
|
|
582
|
-
|
|
583
|
-
// Get total tokens
|
|
584
|
-
const tokensResult = db.exec(
|
|
585
|
-
`SELECT SUM(tokens) as total_tokens
|
|
586
|
-
FROM session_snapshots
|
|
587
|
-
WHERE created_at >= ?`,
|
|
588
|
-
[cutoff]
|
|
589
|
-
);
|
|
590
|
-
|
|
591
|
-
return {
|
|
592
|
-
cpu: cpuResult.length > 0 && cpuResult[0].values.length > 0 ? {
|
|
593
|
-
avgUser: cpuResult[0].values[0][0] || 0,
|
|
594
|
-
maxUser: cpuResult[0].values[0][1] || 0,
|
|
595
|
-
avgLoad: cpuResult[0].values[0][2] || 0
|
|
596
|
-
} : null,
|
|
597
|
-
memory: memResult.length > 0 && memResult[0].values.length > 0 ? {
|
|
598
|
-
avgUsed: memResult[0].values[0][0] || 0,
|
|
599
|
-
maxUsed: memResult[0].values[0][1] || 0,
|
|
600
|
-
avgAvailable: memResult[0].values[0][2] || 0
|
|
601
|
-
} : null,
|
|
602
|
-
sessions: sessionResult.length > 0 && sessionResult[0].values.length > 0 ? {
|
|
603
|
-
count: sessionResult[0].values[0][0] || 0
|
|
604
|
-
} : { count: 0 },
|
|
605
|
-
tokens: tokensResult.length > 0 && tokensResult[0].values.length > 0 ? {
|
|
606
|
-
total: tokensResult[0].values[0][0] || 0
|
|
607
|
-
} : { total: 0 }
|
|
608
|
-
};
|
|
609
|
-
} catch (err) {
|
|
610
|
-
logger.error('Failed to get metrics summary: ' + err.message);
|
|
611
|
-
return null;
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
/**
|
|
616
|
-
* Clean up old data beyond retention period
|
|
617
|
-
* @param {number} days - Number of days to retain
|
|
618
|
-
*/
|
|
619
|
-
export function cleanupOldData(days = 30) {
|
|
620
|
-
if (!db) return;
|
|
621
|
-
|
|
622
|
-
try {
|
|
623
|
-
const cutoff = Date.now() - (days * 24 * 60 * 60 * 1000);
|
|
624
|
-
|
|
625
|
-
db.run('DELETE FROM session_snapshots WHERE created_at < ?', [cutoff]);
|
|
626
|
-
db.run('DELETE FROM cpu_metrics WHERE timestamp < ?', [cutoff]);
|
|
627
|
-
db.run('DELETE FROM memory_metrics WHERE timestamp < ?', [cutoff]);
|
|
628
|
-
db.run('DELETE FROM network_metrics WHERE timestamp < ?', [cutoff]);
|
|
629
|
-
|
|
630
|
-
logger.info('Cleaned up data older than ' + days + ' days');
|
|
631
|
-
} catch (err) {
|
|
632
|
-
logger.error('Failed to cleanup old data: ' + err.message);
|
|
633
|
-
}
|
|
634
|
-
|
|
635
|
-
// Save after cleanup, with its own try-catch
|
|
636
|
-
try {
|
|
637
|
-
saveDatabase();
|
|
638
|
-
} catch (err) {
|
|
639
|
-
logger.error('Failed to save database after cleanup: ' + err.message);
|
|
640
|
-
}
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
/**
|
|
644
|
-
* Store all metrics at once (called from refresh cycle)
|
|
645
|
-
* @param {Object} data - Dashboard data object
|
|
646
|
-
*/
|
|
647
|
-
export function storeMetricsSnapshot(data) {
|
|
648
|
-
if (!data) return;
|
|
649
|
-
|
|
650
|
-
// Store CPU metrics
|
|
651
|
-
if (data.cpu) {
|
|
652
|
-
// Handle both array format and object format
|
|
653
|
-
const cpuData = Array.isArray(data.cpu) ? { cpus: data.cpu } : data.cpu;
|
|
654
|
-
storeCpuMetrics(cpuData);
|
|
655
|
-
}
|
|
656
|
-
|
|
657
|
-
// Store memory metrics
|
|
658
|
-
if (data.memory) {
|
|
659
|
-
storeMemoryMetrics(data.memory);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// Store network metrics
|
|
663
|
-
if (data.network) {
|
|
664
|
-
storeNetworkMetrics(data.network);
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Store session snapshots
|
|
668
|
-
if (data.sessions && data.sessions.length > 0) {
|
|
669
|
-
for (const session of data.sessions) {
|
|
670
|
-
storeSessionSnapshot(session);
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
/**
|
|
676
|
-
* Extract agent name from session key
|
|
677
|
-
* @param {string} key - Session key (e.g., "agent:main:main")
|
|
678
|
-
* @returns {string} Agent name
|
|
679
|
-
*/
|
|
680
|
-
function extractAgent(key) {
|
|
681
|
-
if (!key) return 'unknown';
|
|
682
|
-
const parts = key.split(':');
|
|
683
|
-
return parts[1] || parts[0] || 'unknown';
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
/**
|
|
687
|
-
* Close and save the database
|
|
688
|
-
*/
|
|
689
|
-
export function closeDatabase() {
|
|
690
|
-
// Clear intervals first
|
|
691
|
-
if (saveInterval) {
|
|
692
|
-
clearInterval(saveInterval);
|
|
693
|
-
saveInterval = null;
|
|
694
|
-
}
|
|
695
|
-
if (cleanupInterval) {
|
|
696
|
-
clearInterval(cleanupInterval);
|
|
697
|
-
cleanupInterval = null;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
if (db) {
|
|
701
|
-
try {
|
|
702
|
-
saveDatabase();
|
|
703
|
-
} catch (err) {
|
|
704
|
-
logger.error('Failed to save database during close: ' + err.message);
|
|
705
|
-
}
|
|
706
|
-
try {
|
|
707
|
-
db.close();
|
|
708
|
-
} catch (err) {
|
|
709
|
-
logger.error('Failed to close database: ' + err.message);
|
|
710
|
-
}
|
|
711
|
-
db = null;
|
|
712
|
-
logger.info('Database closed');
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
// Export default object
|
|
717
|
-
export default {
|
|
718
|
-
initDatabase,
|
|
719
|
-
closeDatabase,
|
|
720
|
-
storeSessionSnapshot,
|
|
721
|
-
storeCpuMetrics,
|
|
722
|
-
storeMemoryMetrics,
|
|
723
|
-
storeNetworkMetrics,
|
|
724
|
-
storeMetricsSnapshot,
|
|
725
|
-
getSessionsLast24Hours,
|
|
726
|
-
getSessionsLast7Days,
|
|
727
|
-
getSessionsByHours,
|
|
728
|
-
getSessionsByDays,
|
|
729
|
-
getCpuMetricsHistory,
|
|
730
|
-
getMemoryMetricsHistory,
|
|
731
|
-
getNetworkMetricsHistory,
|
|
732
|
-
getMetricsSummary,
|
|
733
|
-
cleanupOldData
|
|
734
|
-
};
|