mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-19 23:06:49 +00:00
fix(scheduled-ops): audit log text, run attribution, prune targets, and pagination (#234)
- Fix "Run Now" audit log showing "Created scheduled task" instead of "Triggered scheduled task" by adding wildcard-based route matching with specificity sorting - Add triggered_by column to track whether runs were started by the scheduler or manually via Run Now - Add configurable prune targets (containers, images, networks, volumes) with checkbox UI - Add pagination to execution history with offset-based navigation - Document Run Now behavior on disabled tasks and add screenshots
This commit is contained in:
@@ -153,6 +153,7 @@ export interface ScheduledTask {
|
||||
next_run_at: number | null;
|
||||
last_status: string | null;
|
||||
last_error: string | null;
|
||||
prune_targets: string | null;
|
||||
}
|
||||
|
||||
export interface ScheduledTaskRun {
|
||||
@@ -163,6 +164,7 @@ export interface ScheduledTaskRun {
|
||||
status: 'running' | 'success' | 'failure';
|
||||
output: string | null;
|
||||
error: string | null;
|
||||
triggered_by: 'scheduler' | 'manual';
|
||||
}
|
||||
|
||||
export class DatabaseService {
|
||||
@@ -395,6 +397,10 @@ export class DatabaseService {
|
||||
maybeAddCol('nodes', 'api_url', "TEXT DEFAULT ''");
|
||||
maybeAddCol('nodes', 'api_token', "TEXT DEFAULT ''");
|
||||
|
||||
// Scheduled operations migrations
|
||||
maybeAddCol('scheduled_task_runs', 'triggered_by', "TEXT NOT NULL DEFAULT 'scheduler'");
|
||||
maybeAddCol('scheduled_tasks', 'prune_targets', 'TEXT DEFAULT NULL');
|
||||
|
||||
// Drop legacy SSH/TLS columns from pre-0.7 databases (no longer read or written)
|
||||
const legacyCols = ['host', 'port', 'ssh_port', 'ssh_user', 'ssh_password', 'ssh_key', 'tls_ca', 'tls_cert', 'tls_key'];
|
||||
for (const col of legacyCols) {
|
||||
@@ -1133,16 +1139,20 @@ export class DatabaseService {
|
||||
).all(now) as ScheduledTask[];
|
||||
}
|
||||
|
||||
public getScheduledTaskRuns(taskId: number, limit = 50): ScheduledTaskRun[] {
|
||||
return this.db.prepare(
|
||||
'SELECT * FROM scheduled_task_runs WHERE task_id = ? ORDER BY started_at DESC LIMIT ?'
|
||||
).all(taskId, limit) as ScheduledTaskRun[];
|
||||
public getScheduledTaskRuns(taskId: number, limit = 20, offset = 0): { runs: ScheduledTaskRun[]; total: number } {
|
||||
const runs = this.db.prepare(
|
||||
'SELECT * FROM scheduled_task_runs WHERE task_id = ? ORDER BY started_at DESC LIMIT ? OFFSET ?'
|
||||
).all(taskId, limit, offset) as ScheduledTaskRun[];
|
||||
const { total } = this.db.prepare(
|
||||
'SELECT COUNT(*) as total FROM scheduled_task_runs WHERE task_id = ?'
|
||||
).get(taskId) as { total: number };
|
||||
return { runs, total };
|
||||
}
|
||||
|
||||
public createScheduledTaskRun(run: Omit<ScheduledTaskRun, 'id'>): number {
|
||||
const result = this.db.prepare(
|
||||
'INSERT INTO scheduled_task_runs (task_id, started_at, completed_at, status, output, error) VALUES (?, ?, ?, ?, ?, ?)'
|
||||
).run(run.task_id, run.started_at, run.completed_at, run.status, run.output, run.error);
|
||||
'INSERT INTO scheduled_task_runs (task_id, started_at, completed_at, status, output, error, triggered_by) VALUES (?, ?, ?, ?, ?, ?, ?)'
|
||||
).run(run.task_id, run.started_at, run.completed_at, run.status, run.output, run.error, run.triggered_by);
|
||||
return result.lastInsertRowid as number;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,6 +67,8 @@ export class SchedulerService {
|
||||
}
|
||||
}
|
||||
|
||||
// Intentionally allows triggering disabled tasks — useful for testing before enabling a schedule.
|
||||
// Manual triggers are attributed as 'manual' in the run record (see triggered_by column).
|
||||
public async triggerTask(taskId: number): Promise<void> {
|
||||
const db = DatabaseService.getInstance();
|
||||
const task = db.getScheduledTask(taskId);
|
||||
@@ -74,13 +76,13 @@ export class SchedulerService {
|
||||
if (this.runningTasks.has(task.id)) throw new Error('Task is already running');
|
||||
this.runningTasks.add(task.id);
|
||||
try {
|
||||
await this.executeTask(task);
|
||||
await this.executeTask(task, 'manual');
|
||||
} finally {
|
||||
this.runningTasks.delete(task.id);
|
||||
}
|
||||
}
|
||||
|
||||
private async executeTask(task: ScheduledTask): Promise<void> {
|
||||
private async executeTask(task: ScheduledTask, triggeredBy: 'scheduler' | 'manual' = 'scheduler'): Promise<void> {
|
||||
const db = DatabaseService.getInstance();
|
||||
const runId = db.createScheduledTaskRun({
|
||||
task_id: task.id,
|
||||
@@ -89,6 +91,7 @@ export class SchedulerService {
|
||||
status: 'running',
|
||||
output: null,
|
||||
error: null,
|
||||
triggered_by: triggeredBy,
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -297,7 +300,11 @@ export class SchedulerService {
|
||||
private async executePrune(task: ScheduledTask): Promise<string> {
|
||||
const nodeId = task.node_id ?? NodeRegistry.getInstance().getDefaultNodeId();
|
||||
const docker = DockerController.getInstance(nodeId);
|
||||
const targets = ['containers', 'images', 'networks', 'volumes'] as const;
|
||||
const allTargets = ['containers', 'images', 'networks', 'volumes'] as const;
|
||||
type PruneTarget = typeof allTargets[number];
|
||||
const targets: PruneTarget[] = task.prune_targets
|
||||
? (JSON.parse(task.prune_targets) as string[]).filter((t): t is PruneTarget => allTargets.includes(t as PruneTarget))
|
||||
: [...allTargets];
|
||||
const results: string[] = [];
|
||||
|
||||
for (const target of targets) {
|
||||
|
||||
Reference in New Issue
Block a user