cli4ai 1.2.5 → 1.2.7
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/dist/cli.js +31 -0
- package/dist/commands/add.js +5 -1
- package/dist/commands/browse.js +65 -52
- package/dist/commands/download.d.ts +7 -0
- package/dist/commands/download.js +36 -0
- package/dist/commands/routines.js +7 -3
- package/dist/commands/secrets.js +37 -24
- package/dist/commands/serve.d.ts +4 -0
- package/dist/commands/serve.js +19 -1
- package/dist/core/execute.d.ts +4 -0
- package/dist/core/execute.js +37 -12
- package/dist/core/remote-client.d.ts +8 -0
- package/dist/core/remote-client.js +67 -0
- package/dist/core/routine-engine.d.ts +90 -6
- package/dist/core/routine-engine.js +766 -221
- package/dist/core/routines.d.ts +5 -0
- package/dist/core/routines.js +20 -0
- package/dist/core/scheduler.d.ts +19 -0
- package/dist/core/scheduler.js +79 -1
- package/dist/dashboard/api/endpoints.d.ts +14 -0
- package/dist/dashboard/api/endpoints.js +562 -0
- package/dist/dashboard/api/websocket.d.ts +133 -0
- package/dist/dashboard/api/websocket.js +278 -0
- package/dist/dashboard/db/index.d.ts +42 -0
- package/dist/dashboard/db/index.js +112 -0
- package/dist/dashboard/db/runs.d.ts +170 -0
- package/dist/dashboard/db/runs.js +475 -0
- package/dist/dashboard/db/schema.d.ts +64 -0
- package/dist/dashboard/db/schema.js +157 -0
- package/dist/server/service.d.ts +8 -0
- package/dist/server/service.js +192 -6
- package/package.json +13 -3
- package/src/dashboard/public/assets/index-DN1hIAMO.css +1 -0
- package/src/dashboard/public/assets/index-pZeAAQwj.js +331 -0
- package/src/dashboard/public/index.html +14 -0
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run History Database Operations
|
|
3
|
+
*
|
|
4
|
+
* CRUD operations for runs, steps, and logs.
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from 'crypto';
|
|
7
|
+
import { getDatabase } from './index.js';
|
|
8
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
9
|
+
// RUN OPERATIONS
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
11
|
+
/**
|
|
12
|
+
* Create a new run record
|
|
13
|
+
*/
|
|
14
|
+
export function createRun(input) {
|
|
15
|
+
try {
|
|
16
|
+
const db = getDatabase();
|
|
17
|
+
const id = randomUUID();
|
|
18
|
+
const now = new Date().toISOString();
|
|
19
|
+
const stmt = db.prepare(`
|
|
20
|
+
INSERT INTO runs (
|
|
21
|
+
id, routine_name, routine_path, started_at, status,
|
|
22
|
+
trigger_type, retry_attempt, vars_json, created_at
|
|
23
|
+
) VALUES (?, ?, ?, ?, 'running', ?, ?, ?, ?)
|
|
24
|
+
`);
|
|
25
|
+
stmt.run(id, input.routineName, input.routinePath ?? null, now, input.triggerType, input.retryAttempt ?? 0, input.vars ? JSON.stringify(input.vars) : null, now);
|
|
26
|
+
return getRun(id);
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
throw new Error(`Failed to create run: ${error instanceof Error ? error.message : String(error)}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Get a run by ID
|
|
34
|
+
*/
|
|
35
|
+
export function getRun(id) {
|
|
36
|
+
const db = getDatabase();
|
|
37
|
+
const stmt = db.prepare('SELECT * FROM runs WHERE id = ?');
|
|
38
|
+
return stmt.get(id) ?? null;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Get a run with all its steps and logs
|
|
42
|
+
*/
|
|
43
|
+
export function getRunWithDetails(id) {
|
|
44
|
+
const run = getRun(id);
|
|
45
|
+
if (!run)
|
|
46
|
+
return null;
|
|
47
|
+
const steps = getRunSteps(id);
|
|
48
|
+
const logs = getRunLogs(id, { limit: 5000 }); // Include up to 5000 log lines
|
|
49
|
+
const logCount = logs.length;
|
|
50
|
+
return {
|
|
51
|
+
...run,
|
|
52
|
+
steps,
|
|
53
|
+
logs,
|
|
54
|
+
logCount,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Update a run record
|
|
59
|
+
*/
|
|
60
|
+
export function updateRun(id, input) {
|
|
61
|
+
try {
|
|
62
|
+
const db = getDatabase();
|
|
63
|
+
const updates = [];
|
|
64
|
+
const values = [];
|
|
65
|
+
if (input.status !== undefined) {
|
|
66
|
+
updates.push('status = ?');
|
|
67
|
+
values.push(input.status);
|
|
68
|
+
}
|
|
69
|
+
if (input.exitCode !== undefined) {
|
|
70
|
+
updates.push('exit_code = ?');
|
|
71
|
+
values.push(input.exitCode);
|
|
72
|
+
}
|
|
73
|
+
if (input.finishedAt !== undefined) {
|
|
74
|
+
updates.push('finished_at = ?');
|
|
75
|
+
values.push(input.finishedAt);
|
|
76
|
+
}
|
|
77
|
+
if (input.durationMs !== undefined) {
|
|
78
|
+
updates.push('duration_ms = ?');
|
|
79
|
+
values.push(input.durationMs);
|
|
80
|
+
}
|
|
81
|
+
if (updates.length === 0) {
|
|
82
|
+
return getRun(id);
|
|
83
|
+
}
|
|
84
|
+
values.push(id);
|
|
85
|
+
const stmt = db.prepare(`UPDATE runs SET ${updates.join(', ')} WHERE id = ?`);
|
|
86
|
+
stmt.run(...values);
|
|
87
|
+
return getRun(id);
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
throw new Error(`Failed to update run ${id}: ${error instanceof Error ? error.message : String(error)}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Finish a run (set status, exit code, timing)
|
|
95
|
+
*/
|
|
96
|
+
export function finishRun(id, status, exitCode) {
|
|
97
|
+
const run = getRun(id);
|
|
98
|
+
if (!run)
|
|
99
|
+
return null;
|
|
100
|
+
const finishedAt = new Date().toISOString();
|
|
101
|
+
const startedAt = new Date(run.started_at).getTime();
|
|
102
|
+
const durationMs = Date.now() - startedAt;
|
|
103
|
+
return updateRun(id, {
|
|
104
|
+
status,
|
|
105
|
+
exitCode,
|
|
106
|
+
finishedAt,
|
|
107
|
+
durationMs,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* List runs with optional filters
|
|
112
|
+
*/
|
|
113
|
+
export function listRuns(filters = {}) {
|
|
114
|
+
const db = getDatabase();
|
|
115
|
+
const conditions = [];
|
|
116
|
+
const values = [];
|
|
117
|
+
if (filters.routineName) {
|
|
118
|
+
conditions.push('routine_name = ?');
|
|
119
|
+
values.push(filters.routineName);
|
|
120
|
+
}
|
|
121
|
+
if (filters.status) {
|
|
122
|
+
conditions.push('status = ?');
|
|
123
|
+
values.push(filters.status);
|
|
124
|
+
}
|
|
125
|
+
if (filters.triggerType) {
|
|
126
|
+
conditions.push('trigger_type = ?');
|
|
127
|
+
values.push(filters.triggerType);
|
|
128
|
+
}
|
|
129
|
+
if (filters.startedAfter) {
|
|
130
|
+
conditions.push('started_at >= ?');
|
|
131
|
+
values.push(filters.startedAfter);
|
|
132
|
+
}
|
|
133
|
+
if (filters.startedBefore) {
|
|
134
|
+
conditions.push('started_at <= ?');
|
|
135
|
+
values.push(filters.startedBefore);
|
|
136
|
+
}
|
|
137
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
138
|
+
const limit = filters.limit ?? 50;
|
|
139
|
+
const offset = filters.offset ?? 0;
|
|
140
|
+
const stmt = db.prepare(`
|
|
141
|
+
SELECT * FROM runs
|
|
142
|
+
${where}
|
|
143
|
+
ORDER BY started_at DESC
|
|
144
|
+
LIMIT ? OFFSET ?
|
|
145
|
+
`);
|
|
146
|
+
values.push(limit, offset);
|
|
147
|
+
return stmt.all(...values);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Count runs matching filters
|
|
151
|
+
*/
|
|
152
|
+
export function countRuns(filters = {}) {
|
|
153
|
+
const db = getDatabase();
|
|
154
|
+
const conditions = [];
|
|
155
|
+
const values = [];
|
|
156
|
+
if (filters.routineName) {
|
|
157
|
+
conditions.push('routine_name = ?');
|
|
158
|
+
values.push(filters.routineName);
|
|
159
|
+
}
|
|
160
|
+
if (filters.status) {
|
|
161
|
+
conditions.push('status = ?');
|
|
162
|
+
values.push(filters.status);
|
|
163
|
+
}
|
|
164
|
+
if (filters.triggerType) {
|
|
165
|
+
conditions.push('trigger_type = ?');
|
|
166
|
+
values.push(filters.triggerType);
|
|
167
|
+
}
|
|
168
|
+
const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
|
|
169
|
+
const stmt = db.prepare(`SELECT COUNT(*) as count FROM runs ${where}`);
|
|
170
|
+
const result = stmt.get(...values);
|
|
171
|
+
return result.count;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Delete a run and all its associated data
|
|
175
|
+
*/
|
|
176
|
+
export function deleteRun(id) {
|
|
177
|
+
const db = getDatabase();
|
|
178
|
+
const stmt = db.prepare('DELETE FROM runs WHERE id = ?');
|
|
179
|
+
const result = stmt.run(id);
|
|
180
|
+
return result.changes > 0;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Delete old runs (for cleanup)
|
|
184
|
+
*/
|
|
185
|
+
export function deleteOldRuns(olderThanDays) {
|
|
186
|
+
const db = getDatabase();
|
|
187
|
+
const cutoff = new Date();
|
|
188
|
+
cutoff.setDate(cutoff.getDate() - olderThanDays);
|
|
189
|
+
const stmt = db.prepare('DELETE FROM runs WHERE started_at < ?');
|
|
190
|
+
const result = stmt.run(cutoff.toISOString());
|
|
191
|
+
return result.changes;
|
|
192
|
+
}
|
|
193
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
194
|
+
// STEP OPERATIONS
|
|
195
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
196
|
+
/**
|
|
197
|
+
* Create a step record
|
|
198
|
+
*/
|
|
199
|
+
export function createStep(input) {
|
|
200
|
+
try {
|
|
201
|
+
const db = getDatabase();
|
|
202
|
+
const id = randomUUID();
|
|
203
|
+
const now = new Date().toISOString();
|
|
204
|
+
const stmt = db.prepare(`
|
|
205
|
+
INSERT INTO run_steps (
|
|
206
|
+
id, run_id, step_id, step_type, status, exit_code, duration_ms,
|
|
207
|
+
stdout, stderr, json_output, error_code, error_message, created_at
|
|
208
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
209
|
+
`);
|
|
210
|
+
stmt.run(id, input.runId, input.stepId, input.stepType, input.status, input.exitCode ?? null, input.durationMs ?? null, input.stdout ?? null, input.stderr ?? null, input.jsonOutput ? JSON.stringify(input.jsonOutput) : null, input.errorCode ?? null, input.errorMessage ?? null, now);
|
|
211
|
+
return getStep(id);
|
|
212
|
+
}
|
|
213
|
+
catch (error) {
|
|
214
|
+
throw new Error(`Failed to create step ${input.stepId} for run ${input.runId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Get a step by ID
|
|
219
|
+
*/
|
|
220
|
+
export function getStep(id) {
|
|
221
|
+
const db = getDatabase();
|
|
222
|
+
const stmt = db.prepare('SELECT * FROM run_steps WHERE id = ?');
|
|
223
|
+
return stmt.get(id) ?? null;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Get all steps for a run
|
|
227
|
+
*/
|
|
228
|
+
export function getRunSteps(runId) {
|
|
229
|
+
const db = getDatabase();
|
|
230
|
+
const stmt = db.prepare('SELECT * FROM run_steps WHERE run_id = ? ORDER BY created_at');
|
|
231
|
+
return stmt.all(runId);
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Find a step by runId and stepId
|
|
235
|
+
*/
|
|
236
|
+
export function findStepByRunAndStepId(runId, stepId) {
|
|
237
|
+
const db = getDatabase();
|
|
238
|
+
const stmt = db.prepare('SELECT * FROM run_steps WHERE run_id = ? AND step_id = ?');
|
|
239
|
+
return stmt.get(runId, stepId) ?? null;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* Upsert a step (create or update)
|
|
243
|
+
*/
|
|
244
|
+
export function upsertStep(input) {
|
|
245
|
+
const existing = findStepByRunAndStepId(input.runId, input.stepId);
|
|
246
|
+
if (existing) {
|
|
247
|
+
// Update existing step
|
|
248
|
+
return updateStep(existing.id, {
|
|
249
|
+
status: input.status,
|
|
250
|
+
exitCode: input.exitCode,
|
|
251
|
+
durationMs: input.durationMs,
|
|
252
|
+
stdout: input.stdout,
|
|
253
|
+
stderr: input.stderr,
|
|
254
|
+
jsonOutput: input.jsonOutput,
|
|
255
|
+
errorCode: input.errorCode,
|
|
256
|
+
errorMessage: input.errorMessage,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
// Create new step
|
|
261
|
+
return createStep(input);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Update a step
|
|
266
|
+
*/
|
|
267
|
+
export function updateStep(id, updates) {
|
|
268
|
+
const db = getDatabase();
|
|
269
|
+
const fields = [];
|
|
270
|
+
const values = [];
|
|
271
|
+
if (updates.status !== undefined) {
|
|
272
|
+
fields.push('status = ?');
|
|
273
|
+
values.push(updates.status);
|
|
274
|
+
}
|
|
275
|
+
if (updates.exitCode !== undefined) {
|
|
276
|
+
fields.push('exit_code = ?');
|
|
277
|
+
values.push(updates.exitCode);
|
|
278
|
+
}
|
|
279
|
+
if (updates.durationMs !== undefined) {
|
|
280
|
+
fields.push('duration_ms = ?');
|
|
281
|
+
values.push(updates.durationMs);
|
|
282
|
+
}
|
|
283
|
+
if (updates.stdout !== undefined) {
|
|
284
|
+
fields.push('stdout = ?');
|
|
285
|
+
values.push(updates.stdout);
|
|
286
|
+
}
|
|
287
|
+
if (updates.stderr !== undefined) {
|
|
288
|
+
fields.push('stderr = ?');
|
|
289
|
+
values.push(updates.stderr);
|
|
290
|
+
}
|
|
291
|
+
if (updates.jsonOutput !== undefined) {
|
|
292
|
+
fields.push('json_output = ?');
|
|
293
|
+
values.push(JSON.stringify(updates.jsonOutput));
|
|
294
|
+
}
|
|
295
|
+
if (updates.errorCode !== undefined) {
|
|
296
|
+
fields.push('error_code = ?');
|
|
297
|
+
values.push(updates.errorCode);
|
|
298
|
+
}
|
|
299
|
+
if (updates.errorMessage !== undefined) {
|
|
300
|
+
fields.push('error_message = ?');
|
|
301
|
+
values.push(updates.errorMessage);
|
|
302
|
+
}
|
|
303
|
+
if (fields.length === 0)
|
|
304
|
+
return getStep(id);
|
|
305
|
+
values.push(id);
|
|
306
|
+
const stmt = db.prepare(`UPDATE run_steps SET ${fields.join(', ')} WHERE id = ?`);
|
|
307
|
+
stmt.run(...values);
|
|
308
|
+
return getStep(id);
|
|
309
|
+
}
|
|
310
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
311
|
+
// LOG OPERATIONS
|
|
312
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
313
|
+
/**
|
|
314
|
+
* Append a log line
|
|
315
|
+
*/
|
|
316
|
+
export function appendLog(input) {
|
|
317
|
+
const db = getDatabase();
|
|
318
|
+
const now = new Date().toISOString();
|
|
319
|
+
const stmt = db.prepare(`
|
|
320
|
+
INSERT INTO run_logs (run_id, timestamp, stream, line, step_id)
|
|
321
|
+
VALUES (?, ?, ?, ?, ?)
|
|
322
|
+
`);
|
|
323
|
+
stmt.run(input.runId, now, input.stream, input.line, input.stepId ?? null);
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* Append multiple log lines efficiently
|
|
327
|
+
*/
|
|
328
|
+
export function appendLogs(logs) {
|
|
329
|
+
const db = getDatabase();
|
|
330
|
+
const now = new Date().toISOString();
|
|
331
|
+
const stmt = db.prepare(`
|
|
332
|
+
INSERT INTO run_logs (run_id, timestamp, stream, line, step_id)
|
|
333
|
+
VALUES (?, ?, ?, ?, ?)
|
|
334
|
+
`);
|
|
335
|
+
const insertMany = db.transaction((logs) => {
|
|
336
|
+
for (const log of logs) {
|
|
337
|
+
stmt.run(log.runId, now, log.stream, log.line, log.stepId ?? null);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
insertMany(logs);
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Get logs for a run
|
|
344
|
+
*/
|
|
345
|
+
export function getRunLogs(runId, options = {}) {
|
|
346
|
+
const db = getDatabase();
|
|
347
|
+
const { limit = 1000, offset = 0, stream } = options;
|
|
348
|
+
const conditions = ['run_id = ?'];
|
|
349
|
+
const values = [runId];
|
|
350
|
+
if (stream) {
|
|
351
|
+
conditions.push('stream = ?');
|
|
352
|
+
values.push(stream);
|
|
353
|
+
}
|
|
354
|
+
values.push(limit, offset);
|
|
355
|
+
const stmt = db.prepare(`
|
|
356
|
+
SELECT * FROM run_logs
|
|
357
|
+
WHERE ${conditions.join(' AND ')}
|
|
358
|
+
ORDER BY id ASC
|
|
359
|
+
LIMIT ? OFFSET ?
|
|
360
|
+
`);
|
|
361
|
+
return stmt.all(...values);
|
|
362
|
+
}
|
|
363
|
+
/**
|
|
364
|
+
* Get log count for a run
|
|
365
|
+
*/
|
|
366
|
+
export function getRunLogCount(runId) {
|
|
367
|
+
const db = getDatabase();
|
|
368
|
+
const stmt = db.prepare('SELECT COUNT(*) as count FROM run_logs WHERE run_id = ?');
|
|
369
|
+
const result = stmt.get(runId);
|
|
370
|
+
return result.count;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Get the last N logs for a run (for real-time tailing)
|
|
374
|
+
*/
|
|
375
|
+
export function getRecentLogs(runId, count = 100) {
|
|
376
|
+
const db = getDatabase();
|
|
377
|
+
const stmt = db.prepare(`
|
|
378
|
+
SELECT * FROM (
|
|
379
|
+
SELECT * FROM run_logs
|
|
380
|
+
WHERE run_id = ?
|
|
381
|
+
ORDER BY id DESC
|
|
382
|
+
LIMIT ?
|
|
383
|
+
) ORDER BY id ASC
|
|
384
|
+
`);
|
|
385
|
+
return stmt.all(runId, count);
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* Get metrics for a routine
|
|
389
|
+
*/
|
|
390
|
+
export function getRoutineMetrics(routineName) {
|
|
391
|
+
const db = getDatabase();
|
|
392
|
+
const statsStmt = db.prepare(`
|
|
393
|
+
SELECT
|
|
394
|
+
COUNT(*) as total_runs,
|
|
395
|
+
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as successful_runs,
|
|
396
|
+
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failed_runs,
|
|
397
|
+
AVG(duration_ms) as avg_duration_ms,
|
|
398
|
+
MIN(duration_ms) as min_duration_ms,
|
|
399
|
+
MAX(duration_ms) as max_duration_ms
|
|
400
|
+
FROM runs
|
|
401
|
+
WHERE routine_name = ? AND status != 'running'
|
|
402
|
+
`);
|
|
403
|
+
const stats = statsStmt.get(routineName);
|
|
404
|
+
const lastRunStmt = db.prepare(`
|
|
405
|
+
SELECT started_at, status FROM runs
|
|
406
|
+
WHERE routine_name = ?
|
|
407
|
+
ORDER BY started_at DESC
|
|
408
|
+
LIMIT 1
|
|
409
|
+
`);
|
|
410
|
+
const lastRun = lastRunStmt.get(routineName);
|
|
411
|
+
return {
|
|
412
|
+
routineName,
|
|
413
|
+
totalRuns: stats.total_runs,
|
|
414
|
+
successfulRuns: stats.successful_runs,
|
|
415
|
+
failedRuns: stats.failed_runs,
|
|
416
|
+
successRate: stats.total_runs > 0 ? stats.successful_runs / stats.total_runs : 0,
|
|
417
|
+
avgDurationMs: stats.avg_duration_ms,
|
|
418
|
+
minDurationMs: stats.min_duration_ms,
|
|
419
|
+
maxDurationMs: stats.max_duration_ms,
|
|
420
|
+
lastRunAt: lastRun?.started_at ?? null,
|
|
421
|
+
lastStatus: lastRun?.status ?? null,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
/**
|
|
425
|
+
* Get metrics for all routines
|
|
426
|
+
*/
|
|
427
|
+
export function getAllRoutineMetrics() {
|
|
428
|
+
const db = getDatabase();
|
|
429
|
+
const routinesStmt = db.prepare(`
|
|
430
|
+
SELECT DISTINCT routine_name FROM runs ORDER BY routine_name
|
|
431
|
+
`);
|
|
432
|
+
const routines = routinesStmt.all();
|
|
433
|
+
return routines.map((r) => getRoutineMetrics(r.routine_name));
|
|
434
|
+
}
|
|
435
|
+
export function getDashboardStats() {
|
|
436
|
+
const db = getDatabase();
|
|
437
|
+
const today = new Date();
|
|
438
|
+
today.setHours(0, 0, 0, 0);
|
|
439
|
+
const todayStr = today.toISOString();
|
|
440
|
+
const weekAgo = new Date();
|
|
441
|
+
weekAgo.setDate(weekAgo.getDate() - 7);
|
|
442
|
+
weekAgo.setHours(0, 0, 0, 0);
|
|
443
|
+
const weekAgoStr = weekAgo.toISOString();
|
|
444
|
+
const stats = db
|
|
445
|
+
.prepare(`
|
|
446
|
+
SELECT
|
|
447
|
+
COUNT(*) as total_runs,
|
|
448
|
+
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) as running_count,
|
|
449
|
+
SUM(CASE WHEN status = 'success' THEN 1 ELSE 0 END) as success_count,
|
|
450
|
+
SUM(CASE WHEN status = 'failed' THEN 1 ELSE 0 END) as failure_count,
|
|
451
|
+
AVG(CASE WHEN duration_ms IS NOT NULL THEN duration_ms ELSE NULL END) as avg_duration_ms
|
|
452
|
+
FROM runs
|
|
453
|
+
`)
|
|
454
|
+
.get();
|
|
455
|
+
const todayStats = db
|
|
456
|
+
.prepare(`SELECT COUNT(*) as runs_today FROM runs WHERE started_at >= ?`)
|
|
457
|
+
.get(todayStr);
|
|
458
|
+
const weekStats = db
|
|
459
|
+
.prepare(`SELECT COUNT(*) as runs_this_week FROM runs WHERE started_at >= ?`)
|
|
460
|
+
.get(weekAgoStr);
|
|
461
|
+
const totalRuns = stats?.total_runs || 0;
|
|
462
|
+
const successCount = stats?.success_count || 0;
|
|
463
|
+
const failureCount = stats?.failure_count || 0;
|
|
464
|
+
const runningCount = stats?.running_count || 0;
|
|
465
|
+
return {
|
|
466
|
+
totalRuns,
|
|
467
|
+
successCount,
|
|
468
|
+
failureCount,
|
|
469
|
+
runningCount,
|
|
470
|
+
successRate: totalRuns > 0 ? (successCount / totalRuns) * 100 : 0,
|
|
471
|
+
avgDurationMs: stats?.avg_duration_ms || 0,
|
|
472
|
+
runsToday: todayStats?.runs_today || 0,
|
|
473
|
+
runsThisWeek: weekStats?.runs_this_week || 0,
|
|
474
|
+
};
|
|
475
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Database Schema
|
|
3
|
+
*
|
|
4
|
+
* Defines all tables for storing run history, steps, logs, and metrics.
|
|
5
|
+
*/
|
|
6
|
+
import type Database from 'better-sqlite3';
|
|
7
|
+
export type RunStatus = 'running' | 'success' | 'failed';
|
|
8
|
+
export type StepStatus = 'success' | 'failed' | 'skipped' | 'caught' | 'running';
|
|
9
|
+
export type TriggerType = 'scheduled' | 'manual' | 'api';
|
|
10
|
+
export type LogStream = 'stdout' | 'stderr';
|
|
11
|
+
export interface RunRecord {
|
|
12
|
+
id: string;
|
|
13
|
+
routine_name: string;
|
|
14
|
+
routine_path: string | null;
|
|
15
|
+
started_at: string;
|
|
16
|
+
finished_at: string | null;
|
|
17
|
+
status: RunStatus;
|
|
18
|
+
exit_code: number | null;
|
|
19
|
+
duration_ms: number | null;
|
|
20
|
+
trigger_type: TriggerType;
|
|
21
|
+
retry_attempt: number;
|
|
22
|
+
vars_json: string | null;
|
|
23
|
+
created_at: string;
|
|
24
|
+
}
|
|
25
|
+
export interface RunStepRecord {
|
|
26
|
+
id: string;
|
|
27
|
+
run_id: string;
|
|
28
|
+
step_id: string;
|
|
29
|
+
step_type: string;
|
|
30
|
+
status: StepStatus;
|
|
31
|
+
exit_code: number | null;
|
|
32
|
+
duration_ms: number | null;
|
|
33
|
+
stdout: string | null;
|
|
34
|
+
stderr: string | null;
|
|
35
|
+
json_output: string | null;
|
|
36
|
+
error_code: string | null;
|
|
37
|
+
error_message: string | null;
|
|
38
|
+
created_at: string;
|
|
39
|
+
}
|
|
40
|
+
export interface RunLogRecord {
|
|
41
|
+
id: number;
|
|
42
|
+
run_id: string;
|
|
43
|
+
timestamp: string;
|
|
44
|
+
stream: LogStream;
|
|
45
|
+
line: string;
|
|
46
|
+
step_id: string | null;
|
|
47
|
+
}
|
|
48
|
+
export interface MetricRecord {
|
|
49
|
+
id: number;
|
|
50
|
+
routine_name: string;
|
|
51
|
+
period: 'hour' | 'day' | 'week';
|
|
52
|
+
period_start: string;
|
|
53
|
+
total_runs: number;
|
|
54
|
+
successful_runs: number;
|
|
55
|
+
failed_runs: number;
|
|
56
|
+
avg_duration_ms: number | null;
|
|
57
|
+
min_duration_ms: number | null;
|
|
58
|
+
max_duration_ms: number | null;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Initialize the database schema.
|
|
62
|
+
* Creates all tables if they don't exist.
|
|
63
|
+
*/
|
|
64
|
+
export declare function initSchema(db: Database.Database): void;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dashboard Database Schema
|
|
3
|
+
*
|
|
4
|
+
* Defines all tables for storing run history, steps, logs, and metrics.
|
|
5
|
+
*/
|
|
6
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
7
|
+
// SCHEMA VERSION
|
|
8
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
9
|
+
const SCHEMA_VERSION = 1;
|
|
10
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
11
|
+
// SCHEMA INITIALIZATION
|
|
12
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
13
|
+
/**
|
|
14
|
+
* Initialize the database schema.
|
|
15
|
+
* Creates all tables if they don't exist.
|
|
16
|
+
*/
|
|
17
|
+
export function initSchema(db) {
|
|
18
|
+
// Check if we need to migrate
|
|
19
|
+
const version = getSchemaVersion(db);
|
|
20
|
+
if (version === 0) {
|
|
21
|
+
// Fresh install - create all tables
|
|
22
|
+
createTables(db);
|
|
23
|
+
setSchemaVersion(db, SCHEMA_VERSION);
|
|
24
|
+
}
|
|
25
|
+
else if (version < SCHEMA_VERSION) {
|
|
26
|
+
// Run migrations
|
|
27
|
+
migrate(db, version, SCHEMA_VERSION);
|
|
28
|
+
setSchemaVersion(db, SCHEMA_VERSION);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Get current schema version
|
|
33
|
+
*/
|
|
34
|
+
function getSchemaVersion(db) {
|
|
35
|
+
try {
|
|
36
|
+
const result = db.prepare('SELECT version FROM schema_version').get();
|
|
37
|
+
return result?.version ?? 0;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
// Table doesn't exist
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Set schema version
|
|
46
|
+
*/
|
|
47
|
+
function setSchemaVersion(db, version) {
|
|
48
|
+
db.exec(`
|
|
49
|
+
CREATE TABLE IF NOT EXISTS schema_version (
|
|
50
|
+
version INTEGER NOT NULL
|
|
51
|
+
);
|
|
52
|
+
DELETE FROM schema_version;
|
|
53
|
+
`);
|
|
54
|
+
db.prepare('INSERT INTO schema_version (version) VALUES (?)').run(version);
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Create all tables
|
|
58
|
+
*/
|
|
59
|
+
function createTables(db) {
|
|
60
|
+
db.exec(`
|
|
61
|
+
-- ═══════════════════════════════════════════════════════════════════════
|
|
62
|
+
-- RUNS TABLE
|
|
63
|
+
-- ═══════════════════════════════════════════════════════════════════════
|
|
64
|
+
CREATE TABLE IF NOT EXISTS runs (
|
|
65
|
+
id TEXT PRIMARY KEY,
|
|
66
|
+
routine_name TEXT NOT NULL,
|
|
67
|
+
routine_path TEXT,
|
|
68
|
+
started_at TEXT NOT NULL,
|
|
69
|
+
finished_at TEXT,
|
|
70
|
+
status TEXT NOT NULL CHECK (status IN ('running', 'success', 'failed')),
|
|
71
|
+
exit_code INTEGER,
|
|
72
|
+
duration_ms INTEGER,
|
|
73
|
+
trigger_type TEXT NOT NULL CHECK (trigger_type IN ('scheduled', 'manual', 'api')),
|
|
74
|
+
retry_attempt INTEGER DEFAULT 0,
|
|
75
|
+
vars_json TEXT,
|
|
76
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
CREATE INDEX IF NOT EXISTS idx_runs_routine_name ON runs(routine_name);
|
|
80
|
+
CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(status);
|
|
81
|
+
CREATE INDEX IF NOT EXISTS idx_runs_started_at ON runs(started_at DESC);
|
|
82
|
+
CREATE INDEX IF NOT EXISTS idx_runs_trigger_type ON runs(trigger_type);
|
|
83
|
+
CREATE INDEX IF NOT EXISTS idx_runs_routine_status ON runs(routine_name, status);
|
|
84
|
+
|
|
85
|
+
-- ═══════════════════════════════════════════════════════════════════════
|
|
86
|
+
-- RUN STEPS TABLE
|
|
87
|
+
-- ═══════════════════════════════════════════════════════════════════════
|
|
88
|
+
CREATE TABLE IF NOT EXISTS run_steps (
|
|
89
|
+
id TEXT PRIMARY KEY,
|
|
90
|
+
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
91
|
+
step_id TEXT NOT NULL,
|
|
92
|
+
step_type TEXT NOT NULL,
|
|
93
|
+
status TEXT NOT NULL CHECK (status IN ('success', 'failed', 'skipped', 'caught', 'running')),
|
|
94
|
+
exit_code INTEGER,
|
|
95
|
+
duration_ms INTEGER,
|
|
96
|
+
stdout TEXT,
|
|
97
|
+
stderr TEXT,
|
|
98
|
+
json_output TEXT,
|
|
99
|
+
error_code TEXT,
|
|
100
|
+
error_message TEXT,
|
|
101
|
+
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
CREATE INDEX IF NOT EXISTS idx_run_steps_run_id ON run_steps(run_id);
|
|
105
|
+
|
|
106
|
+
-- ═══════════════════════════════════════════════════════════════════════
|
|
107
|
+
-- RUN LOGS TABLE (for streaming logs)
|
|
108
|
+
-- ═══════════════════════════════════════════════════════════════════════
|
|
109
|
+
CREATE TABLE IF NOT EXISTS run_logs (
|
|
110
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
111
|
+
run_id TEXT NOT NULL REFERENCES runs(id) ON DELETE CASCADE,
|
|
112
|
+
timestamp TEXT NOT NULL,
|
|
113
|
+
stream TEXT NOT NULL CHECK (stream IN ('stdout', 'stderr')),
|
|
114
|
+
line TEXT NOT NULL,
|
|
115
|
+
step_id TEXT
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
CREATE INDEX IF NOT EXISTS idx_run_logs_run_id ON run_logs(run_id);
|
|
119
|
+
CREATE INDEX IF NOT EXISTS idx_run_logs_timestamp ON run_logs(timestamp);
|
|
120
|
+
CREATE INDEX IF NOT EXISTS idx_run_logs_run_id_stream ON run_logs(run_id, stream);
|
|
121
|
+
|
|
122
|
+
-- ═══════════════════════════════════════════════════════════════════════
|
|
123
|
+
-- METRICS TABLE (aggregated stats)
|
|
124
|
+
-- ═══════════════════════════════════════════════════════════════════════
|
|
125
|
+
CREATE TABLE IF NOT EXISTS metrics (
|
|
126
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
127
|
+
routine_name TEXT NOT NULL,
|
|
128
|
+
period TEXT NOT NULL CHECK (period IN ('hour', 'day', 'week')),
|
|
129
|
+
period_start TEXT NOT NULL,
|
|
130
|
+
total_runs INTEGER DEFAULT 0,
|
|
131
|
+
successful_runs INTEGER DEFAULT 0,
|
|
132
|
+
failed_runs INTEGER DEFAULT 0,
|
|
133
|
+
avg_duration_ms REAL,
|
|
134
|
+
min_duration_ms INTEGER,
|
|
135
|
+
max_duration_ms INTEGER,
|
|
136
|
+
UNIQUE(routine_name, period, period_start)
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_routine ON metrics(routine_name);
|
|
140
|
+
CREATE INDEX IF NOT EXISTS idx_metrics_period ON metrics(period, period_start);
|
|
141
|
+
`);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Run migrations between versions
|
|
145
|
+
*/
|
|
146
|
+
function migrate(db, fromVersion, toVersion) {
|
|
147
|
+
// Future migrations go here
|
|
148
|
+
// Example:
|
|
149
|
+
// if (fromVersion < 2 && toVersion >= 2) {
|
|
150
|
+
// db.exec('ALTER TABLE runs ADD COLUMN new_field TEXT');
|
|
151
|
+
// }
|
|
152
|
+
// For now, just recreate tables if version mismatch
|
|
153
|
+
// In production, this would be proper migrations
|
|
154
|
+
if (fromVersion < toVersion) {
|
|
155
|
+
createTables(db);
|
|
156
|
+
}
|
|
157
|
+
}
|