feat: Implement Cloudly Task Manager with predefined tasks and execution tracking
- Added CloudlyTaskManager class for managing tasks, including registration, execution, scheduling, and cancellation. - Created predefined tasks: DNS Sync, Certificate Renewal, Cleanup, Health Check, Resource Report, Database Maintenance, Security Scan, and Docker Cleanup. - Introduced ITaskExecution interface for tracking task execution details and outcomes. - Developed API request interfaces for task management operations (getTasks, getTaskExecutions, triggerTask, cancelTask). - Implemented CloudlyViewTasks web component for displaying tasks and their execution history, including filtering and detailed views.
This commit is contained in:
		| @@ -16,7 +16,7 @@ import { MongodbConnector } from './connector.mongodb/connector.js'; | ||||
| // processes | ||||
| import { CloudlyCoreflowManager } from './manager.coreflow/coreflowmanager.js'; | ||||
| import { ClusterManager } from './manager.cluster/classes.clustermanager.js'; | ||||
| import { CloudlyTaskmanager } from './manager.task/taskmanager.js'; | ||||
| import { CloudlyTaskManager } from './manager.task/classes.taskmanager.js'; | ||||
| import { CloudlySecretManager } from './manager.secret/classes.secretmanager.js'; | ||||
| import { CloudlyNodeManager } from './manager.node/classes.nodemanager.js'; | ||||
| import { CloudlyBaremetalManager } from './manager.baremetal/classes.baremetalmanager.js'; | ||||
| @@ -68,7 +68,7 @@ export class Cloudly { | ||||
|   public deploymentManager: DeploymentManager; | ||||
|   public dnsManager: DnsManager; | ||||
|   public domainManager: DomainManager; | ||||
|   public taskManager: CloudlyTaskmanager; | ||||
|   public taskManager: CloudlyTaskManager; | ||||
|   public nodeManager: CloudlyNodeManager; | ||||
|   public baremetalManager: CloudlyBaremetalManager; | ||||
|  | ||||
| @@ -101,7 +101,7 @@ export class Cloudly { | ||||
|     this.deploymentManager = new DeploymentManager(this); | ||||
|     this.dnsManager = new DnsManager(this); | ||||
|     this.domainManager = new DomainManager(this); | ||||
|     this.taskManager = new CloudlyTaskmanager(this); | ||||
|     this.taskManager = new CloudlyTaskManager(this); | ||||
|     this.secretManager = new CloudlySecretManager(this); | ||||
|     this.nodeManager = new CloudlyNodeManager(this); | ||||
|     this.baremetalManager = new CloudlyBaremetalManager(this); | ||||
| @@ -128,6 +128,7 @@ export class Cloudly { | ||||
|     await this.baremetalManager.start(); | ||||
|     await this.serviceManager.start(); | ||||
|     await this.deploymentManager.start(); | ||||
|     await this.taskManager.init(); | ||||
|      | ||||
|     await this.cloudflareConnector.init(); | ||||
|     await this.letsencryptConnector.init(); | ||||
| @@ -150,6 +151,7 @@ export class Cloudly { | ||||
|     await this.secretManager.stop(); | ||||
|     await this.serviceManager.stop(); | ||||
|     await this.deploymentManager.stop(); | ||||
|     await this.taskManager.stop(); | ||||
|     await this.externalRegistryManager.stop(); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
|  | ||||
| @plugins.smartdata.managed() | ||||
| export class Deployment extends plugins.smartdata.SmartDataDbDoc< | ||||
|   Deployment, | ||||
|   plugins.servezoneInterfaces.data.IDeployment | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { DnsManager } from './classes.dnsmanager.js'; | ||||
|  | ||||
| @plugins.smartdata.managed() | ||||
| export class DnsEntry extends plugins.smartdata.SmartDataDbDoc< | ||||
|   DnsEntry, | ||||
|   plugins.servezoneInterfaces.data.IDnsEntry, | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { DomainManager } from './classes.domainmanager.js'; | ||||
|  | ||||
| @plugins.smartdata.managed() | ||||
| export class Domain extends plugins.smartdata.SmartDataDbDoc< | ||||
|   Domain, | ||||
|   plugins.servezoneInterfaces.data.IDomain, | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import * as paths from '../paths.js'; | ||||
| import type { Cloudly } from 'ts/classes.cloudly.js'; | ||||
| import type { ExternalRegistryManager } from './classes.externalregistrymanager.js'; | ||||
|  | ||||
| @plugins.smartdata.managed() | ||||
| export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalRegistry, plugins.servezoneInterfaces.data.IExternalRegistry, ExternalRegistryManager> { | ||||
|   // STATIC | ||||
|   public static async getRegistryById(registryIdArg: string) { | ||||
| @@ -27,7 +28,7 @@ export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalR | ||||
|  | ||||
|   public static async createExternalRegistry(registryDataArg: Partial<plugins.servezoneInterfaces.data.IExternalRegistry['data']>) { | ||||
|     const externalRegistry = new ExternalRegistry(); | ||||
|     externalRegistry.id = await ExternalRegistry.getNewId(); | ||||
|     externalRegistry.id = await this.getNewId(); | ||||
|     externalRegistry.data = { | ||||
|       type: registryDataArg.type || 'docker', | ||||
|       name: registryDataArg.name || '', | ||||
| @@ -48,7 +49,7 @@ export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalR | ||||
|      | ||||
|     // If this is set as default, unset other defaults of the same type | ||||
|     if (externalRegistry.data.isDefault) { | ||||
|       const existingDefaults = await ExternalRegistry.getInstances({ | ||||
|       const existingDefaults = await this.getInstances({ | ||||
|         'data.type': externalRegistry.data.type, | ||||
|         'data.isDefault': true, | ||||
|       }); | ||||
| @@ -73,7 +74,7 @@ export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalR | ||||
|      | ||||
|     // If setting as default, unset other defaults of the same type | ||||
|     if (registryDataArg.isDefault && !externalRegistry.data.isDefault) { | ||||
|       const existingDefaults = await ExternalRegistry.getInstances({ | ||||
|       const existingDefaults = await this.getInstances({ | ||||
|         'data.type': externalRegistry.data.type, | ||||
|         'data.isDefault': true, | ||||
|       }); | ||||
|   | ||||
| @@ -24,7 +24,7 @@ export class ExternalRegistryManager { | ||||
|           this.cloudlyRef.authManager.validIdentityGuard, | ||||
|         ]); | ||||
|          | ||||
|         const registry = await ExternalRegistry.getRegistryById(dataArg.id); | ||||
|         const registry = await this.CExternalRegistry.getRegistryById(dataArg.id); | ||||
|         if (!registry) { | ||||
|           throw new Error(`Registry with id ${dataArg.id} not found`); | ||||
|         } | ||||
| @@ -41,7 +41,7 @@ export class ExternalRegistryManager { | ||||
|           this.cloudlyRef.authManager.validIdentityGuard, | ||||
|         ]); | ||||
|          | ||||
|         const registries = await ExternalRegistry.getRegistries(); | ||||
|         const registries = await this.CExternalRegistry.getRegistries(); | ||||
|         return { | ||||
|           registries: await Promise.all( | ||||
|             registries.map((registry) => registry.createSavableObject()) | ||||
| @@ -57,7 +57,7 @@ export class ExternalRegistryManager { | ||||
|           this.cloudlyRef.authManager.validIdentityGuard, | ||||
|         ]); | ||||
|          | ||||
|         const registry = await ExternalRegistry.createExternalRegistry(dataArg.registryData); | ||||
|         const registry = await this.CExternalRegistry.createExternalRegistry(dataArg.registryData); | ||||
|         return { | ||||
|           registry: await registry.createSavableObject(), | ||||
|         }; | ||||
| @@ -71,7 +71,7 @@ export class ExternalRegistryManager { | ||||
|           this.cloudlyRef.authManager.validIdentityGuard, | ||||
|         ]); | ||||
|          | ||||
|         const registry = await ExternalRegistry.updateExternalRegistry( | ||||
|         const registry = await this.CExternalRegistry.updateExternalRegistry( | ||||
|           dataArg.registryId, | ||||
|           dataArg.registryData | ||||
|         ); | ||||
| @@ -88,7 +88,7 @@ export class ExternalRegistryManager { | ||||
|           this.cloudlyRef.authManager.validIdentityGuard, | ||||
|         ]); | ||||
|          | ||||
|         const success = await ExternalRegistry.deleteExternalRegistry(dataArg.registryId); | ||||
|         const success = await this.CExternalRegistry.deleteExternalRegistry(dataArg.registryId); | ||||
|         return { | ||||
|           ok: success, | ||||
|         }; | ||||
| @@ -102,7 +102,7 @@ export class ExternalRegistryManager { | ||||
|           this.cloudlyRef.authManager.validIdentityGuard, | ||||
|         ]); | ||||
|          | ||||
|         const registry = await ExternalRegistry.getRegistryById(dataArg.registryId); | ||||
|         const registry = await this.CExternalRegistry.getRegistryById(dataArg.registryId); | ||||
|         if (!registry) { | ||||
|           return { | ||||
|             success: false, | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { SecretBundle } from 'ts/manager.secret/classes.secretbundle.js'; | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { ServiceManager } from './classes.servicemanager.js'; | ||||
|  | ||||
| @plugins.smartdata.managed() | ||||
| export class Service extends plugins.smartdata.SmartDataDbDoc< | ||||
|   Service, | ||||
|   plugins.servezoneInterfaces.data.IService, | ||||
|   | ||||
							
								
								
									
										165
									
								
								ts/manager.task/classes.taskexecution.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								ts/manager.task/classes.taskexecution.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,165 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { CloudlyTaskManager } from './classes.taskmanager.js'; | ||||
|  | ||||
| @plugins.smartdata.managed() | ||||
| export class TaskExecution extends plugins.smartdata.SmartDataDbDoc< | ||||
|   TaskExecution, | ||||
|   plugins.servezoneInterfaces.data.ITaskExecution, | ||||
|   CloudlyTaskManager | ||||
| > { | ||||
|   // STATIC | ||||
|   public static async getTaskExecutionById(executionIdArg: string) { | ||||
|     const execution = await this.getInstance({ | ||||
|       id: executionIdArg, | ||||
|     }); | ||||
|     return execution; | ||||
|   } | ||||
|  | ||||
|   public static async getTaskExecutions(filterArg?: { | ||||
|     taskName?: string; | ||||
|     status?: string; | ||||
|     startedAfter?: number; | ||||
|     startedBefore?: number; | ||||
|   }) { | ||||
|     const query: any = {}; | ||||
|      | ||||
|     if (filterArg?.taskName) { | ||||
|       query['data.taskName'] = filterArg.taskName; | ||||
|     } | ||||
|     if (filterArg?.status) { | ||||
|       query['data.status'] = filterArg.status; | ||||
|     } | ||||
|     if (filterArg?.startedAfter || filterArg?.startedBefore) { | ||||
|       query['data.startedAt'] = {}; | ||||
|       if (filterArg.startedAfter) { | ||||
|         query['data.startedAt'].$gte = filterArg.startedAfter; | ||||
|       } | ||||
|       if (filterArg.startedBefore) { | ||||
|         query['data.startedAt'].$lte = filterArg.startedBefore; | ||||
|       } | ||||
|     } | ||||
|      | ||||
|     const executions = await this.getInstances(query); | ||||
|     return executions; | ||||
|   } | ||||
|  | ||||
|   public static async createTaskExecution( | ||||
|     taskName: string, | ||||
|     triggeredBy: 'schedule' | 'manual' | 'system', | ||||
|     userId?: string | ||||
|   ) { | ||||
|     const execution = new TaskExecution(); | ||||
|     execution.id = await TaskExecution.getNewId(); | ||||
|     execution.data = { | ||||
|       taskName, | ||||
|       startedAt: Date.now(), | ||||
|       status: 'running', | ||||
|       triggeredBy, | ||||
|       userId, | ||||
|       logs: [], | ||||
|     }; | ||||
|     await execution.save(); | ||||
|     return execution; | ||||
|   } | ||||
|  | ||||
|   // INSTANCE | ||||
|   @plugins.smartdata.svDb() | ||||
|   public id: string; | ||||
|  | ||||
|   @plugins.smartdata.svDb() | ||||
|   public data: plugins.servezoneInterfaces.data.ITaskExecution['data']; | ||||
|  | ||||
|   /** | ||||
|    * Add a log entry to the execution | ||||
|    */ | ||||
|   public async addLog(message: string, severity: 'info' | 'warning' | 'error' | 'success' = 'info') { | ||||
|     this.data.logs.push({ | ||||
|       timestamp: Date.now(), | ||||
|       message, | ||||
|       severity, | ||||
|     }); | ||||
|     await this.save(); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Set a metric for the execution | ||||
|    */ | ||||
|   public async setMetric(key: string, value: any) { | ||||
|     if (!this.data.metrics) { | ||||
|       this.data.metrics = {}; | ||||
|     } | ||||
|     this.data.metrics[key] = value; | ||||
|     await this.save(); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Mark the execution as completed | ||||
|    */ | ||||
|   public async complete(result?: any) { | ||||
|     this.data.completedAt = Date.now(); | ||||
|     this.data.duration = this.data.completedAt - this.data.startedAt; | ||||
|     this.data.status = 'completed'; | ||||
|     if (result !== undefined) { | ||||
|       this.data.result = result; | ||||
|     } | ||||
|     await this.save(); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Mark the execution as failed | ||||
|    */ | ||||
|   public async fail(error: Error | string) { | ||||
|     this.data.completedAt = Date.now(); | ||||
|     this.data.duration = this.data.completedAt - this.data.startedAt; | ||||
|     this.data.status = 'failed'; | ||||
|      | ||||
|     if (typeof error === 'string') { | ||||
|       this.data.error = { | ||||
|         message: error, | ||||
|       }; | ||||
|     } else { | ||||
|       this.data.error = { | ||||
|         message: error.message, | ||||
|         stack: error.stack, | ||||
|         code: (error as any).code, | ||||
|       }; | ||||
|     } | ||||
|      | ||||
|     await this.save(); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Cancel the execution | ||||
|    */ | ||||
|   public async cancel() { | ||||
|     this.data.completedAt = Date.now(); | ||||
|     this.data.duration = this.data.completedAt - this.data.startedAt; | ||||
|     this.data.status = 'cancelled'; | ||||
|     await this.save(); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get running executions | ||||
|    */ | ||||
|   public static async getRunningExecutions() { | ||||
|     return await this.getInstances({ | ||||
|       'data.status': 'running', | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Clean up old executions | ||||
|    */ | ||||
|   public static async cleanupOldExecutions(olderThanDays: number = 30) { | ||||
|     const cutoffTime = Date.now() - (olderThanDays * 24 * 60 * 60 * 1000); | ||||
|     const oldExecutions = await this.getInstances({ | ||||
|       'data.completedAt': { $lt: cutoffTime }, | ||||
|     }); | ||||
|      | ||||
|     for (const execution of oldExecutions) { | ||||
|       await execution.delete(); | ||||
|     } | ||||
|      | ||||
|     return oldExecutions.length; | ||||
|   } | ||||
| } | ||||
							
								
								
									
										330
									
								
								ts/manager.task/classes.taskmanager.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										330
									
								
								ts/manager.task/classes.taskmanager.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,330 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { Cloudly } from '../classes.cloudly.js'; | ||||
| import { TaskExecution } from './classes.taskexecution.js'; | ||||
| import { createPredefinedTasks } from './predefinedtasks.js'; | ||||
| import { logger } from '../logger.js'; | ||||
|  | ||||
| export interface ITaskInfo { | ||||
|   name: string; | ||||
|   description: string; | ||||
|   category: 'maintenance' | 'deployment' | 'backup' | 'monitoring' | 'cleanup' | 'system' | 'security'; | ||||
|   schedule?: string; // Cron expression if scheduled | ||||
|   lastRun?: number; | ||||
|   enabled: boolean; | ||||
| } | ||||
|  | ||||
| export class CloudlyTaskManager { | ||||
|   public typedrouter = new plugins.typedrequest.TypedRouter(); | ||||
|   public cloudlyRef: Cloudly; | ||||
|    | ||||
|   // TaskBuffer integration | ||||
|   private taskBufferManager = new plugins.taskbuffer.TaskManager(); | ||||
|   private taskRegistry = new Map<string, plugins.taskbuffer.Task>(); | ||||
|   private taskInfo = new Map<string, ITaskInfo>(); | ||||
|   private currentExecutions = new Map<string, TaskExecution>(); | ||||
|    | ||||
|   // Database connection helper | ||||
|   get db() { | ||||
|     return this.cloudlyRef.mongodbConnector.smartdataDb; | ||||
|   } | ||||
|    | ||||
|   // Set up TaskExecution document manager | ||||
|   public CTaskExecution = plugins.smartdata.setDefaultManagerForDoc(this, TaskExecution); | ||||
|  | ||||
|   constructor(cloudlyRefArg: Cloudly) { | ||||
|     this.cloudlyRef = cloudlyRefArg; | ||||
|      | ||||
|     // Add router to main router | ||||
|     this.cloudlyRef.typedrouter.addTypedRouter(this.typedrouter); | ||||
|      | ||||
|     // Set up API endpoints | ||||
|     this.setupApiEndpoints(); | ||||
|      | ||||
|     // Register predefined tasks | ||||
|     createPredefinedTasks(this); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Register a task with the manager | ||||
|    */ | ||||
|   public registerTask( | ||||
|     name: string, | ||||
|     task: plugins.taskbuffer.Task, | ||||
|     info: Omit<ITaskInfo, 'name' | 'lastRun'> | ||||
|   ) { | ||||
|     this.taskRegistry.set(name, task); | ||||
|     this.taskInfo.set(name, { | ||||
|       name, | ||||
|       ...info, | ||||
|       lastRun: undefined, | ||||
|     }); | ||||
|      | ||||
|     // Schedule if cron expression provided | ||||
|     if (info.schedule && info.enabled) { | ||||
|       this.scheduleTask(name, info.schedule); | ||||
|     } | ||||
|      | ||||
|     logger.log('info', `Registered task: ${name}`); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Execute a task with tracking | ||||
|    */ | ||||
|   public async executeTask( | ||||
|     taskName: string, | ||||
|     triggeredBy: 'schedule' | 'manual' | 'system', | ||||
|     userId?: string | ||||
|   ): Promise<TaskExecution> { | ||||
|     const task = this.taskRegistry.get(taskName); | ||||
|     const info = this.taskInfo.get(taskName); | ||||
|      | ||||
|     if (!task) { | ||||
|       throw new Error(`Task ${taskName} not found`); | ||||
|     } | ||||
|      | ||||
|     if (!info?.enabled && triggeredBy === 'schedule') { | ||||
|       logger.log('warn', `Skipping disabled scheduled task: ${taskName}`); | ||||
|       return null; | ||||
|     } | ||||
|      | ||||
|     // Create execution record | ||||
|     const execution = await TaskExecution.createTaskExecution(taskName, triggeredBy, userId); | ||||
|      | ||||
|     if (info?.description) { | ||||
|       execution.data.taskDescription = info.description; | ||||
|     } | ||||
|     if (info?.category) { | ||||
|       execution.data.category = info.category; | ||||
|     } | ||||
|     await execution.save(); | ||||
|      | ||||
|     // Store current execution for task to access | ||||
|     this.currentExecutions.set(taskName, execution); | ||||
|      | ||||
|     try { | ||||
|       await execution.addLog(`Starting task: ${taskName}`, 'info'); | ||||
|        | ||||
|       // Execute the task | ||||
|       const result = await task.trigger(); | ||||
|        | ||||
|       // Task completed successfully | ||||
|       await execution.complete(result); | ||||
|       await execution.addLog(`Task completed successfully`, 'success'); | ||||
|        | ||||
|       // Update last run time | ||||
|       if (info) { | ||||
|         info.lastRun = Date.now(); | ||||
|       } | ||||
|        | ||||
|     } catch (error) { | ||||
|       // Task failed | ||||
|       await execution.fail(error); | ||||
|       await execution.addLog(`Task failed: ${error.message}`, 'error'); | ||||
|       logger.log('error', `Task ${taskName} failed: ${error.message}`); | ||||
|     } finally { | ||||
|       // Clean up current execution | ||||
|       this.currentExecutions.delete(taskName); | ||||
|     } | ||||
|      | ||||
|     return execution; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get current execution for a task (used by tasks to log) | ||||
|    */ | ||||
|   public getCurrentExecution(taskName: string): TaskExecution | undefined { | ||||
|     return this.currentExecutions.get(taskName); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Schedule a task with cron expression | ||||
|    */ | ||||
|   public scheduleTask(taskName: string, cronExpression: string) { | ||||
|     const task = this.taskRegistry.get(taskName); | ||||
|     if (!task) { | ||||
|       throw new Error(`Task ${taskName} not found`); | ||||
|     } | ||||
|      | ||||
|     // Wrap task execution with tracking | ||||
|     const wrappedTask = new plugins.taskbuffer.Task({ | ||||
|       name: `${taskName}-scheduled`, | ||||
|       taskFunction: async () => { | ||||
|         await this.executeTask(taskName, 'schedule'); | ||||
|       }, | ||||
|     }); | ||||
|      | ||||
|     this.taskBufferManager.addAndScheduleTask(wrappedTask, cronExpression); | ||||
|     logger.log('info', `Scheduled task ${taskName} with cron: ${cronExpression}`); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Cancel a running task | ||||
|    */ | ||||
|   public async cancelTask(executionId: string): Promise<boolean> { | ||||
|     const execution = await TaskExecution.getTaskExecutionById(executionId); | ||||
|     if (!execution || execution.data.status !== 'running') { | ||||
|       return false; | ||||
|     } | ||||
|      | ||||
|     await execution.cancel(); | ||||
|     await execution.addLog('Task cancelled by user', 'warning'); | ||||
|      | ||||
|     // TODO: Implement actual task cancellation in taskbuffer | ||||
|      | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get all registered tasks | ||||
|    */ | ||||
|   public getAllTasks(): ITaskInfo[] { | ||||
|     return Array.from(this.taskInfo.values()); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Enable or disable a task | ||||
|    */ | ||||
|   public async setTaskEnabled(taskName: string, enabled: boolean) { | ||||
|     const info = this.taskInfo.get(taskName); | ||||
|     if (!info) { | ||||
|       throw new Error(`Task ${taskName} not found`); | ||||
|     } | ||||
|      | ||||
|     info.enabled = enabled; | ||||
|      | ||||
|     if (!enabled) { | ||||
|       // TODO: Remove from scheduler if disabled | ||||
|       logger.log('info', `Disabled task: ${taskName}`); | ||||
|     } else if (info.schedule) { | ||||
|       // Reschedule if enabled with schedule | ||||
|       this.scheduleTask(taskName, info.schedule); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Set up API endpoints | ||||
|    */ | ||||
|   private setupApiEndpoints() { | ||||
|     // Get all tasks | ||||
|     this.typedrouter.addTypedHandler( | ||||
|       new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTasks>( | ||||
|         'getTasks', | ||||
|         async (reqArg) => { | ||||
|           await plugins.smartguard.passGuardsOrReject(reqArg, [ | ||||
|             this.cloudlyRef.authManager.validIdentityGuard, | ||||
|           ]); | ||||
|            | ||||
|           const tasks = this.getAllTasks(); | ||||
|            | ||||
|           return { | ||||
|             tasks, | ||||
|           }; | ||||
|         } | ||||
|       ) | ||||
|     ); | ||||
|      | ||||
|     // Get task executions | ||||
|     this.typedrouter.addTypedHandler( | ||||
|       new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTaskExecutions>( | ||||
|         'getTaskExecutions', | ||||
|         async (reqArg) => { | ||||
|           await plugins.smartguard.passGuardsOrReject(reqArg, [ | ||||
|             this.cloudlyRef.authManager.validIdentityGuard, | ||||
|           ]); | ||||
|            | ||||
|           const executions = await TaskExecution.getTaskExecutions(reqArg.filter); | ||||
|            | ||||
|           return { | ||||
|             executions: await Promise.all( | ||||
|               executions.map(e => e.createSavableObject()) | ||||
|             ), | ||||
|           }; | ||||
|         } | ||||
|       ) | ||||
|     ); | ||||
|      | ||||
|     // Get task execution by ID | ||||
|     this.typedrouter.addTypedHandler( | ||||
|       new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_GetTaskExecutionById>( | ||||
|         'getTaskExecutionById', | ||||
|         async (reqArg) => { | ||||
|           await plugins.smartguard.passGuardsOrReject(reqArg, [ | ||||
|             this.cloudlyRef.authManager.validIdentityGuard, | ||||
|           ]); | ||||
|            | ||||
|           const execution = await TaskExecution.getTaskExecutionById(reqArg.executionId); | ||||
|            | ||||
|           if (!execution) { | ||||
|             throw new Error('Task execution not found'); | ||||
|           } | ||||
|            | ||||
|           return { | ||||
|             execution: await execution.createSavableObject(), | ||||
|           }; | ||||
|         } | ||||
|       ) | ||||
|     ); | ||||
|      | ||||
|     // Trigger task manually | ||||
|     this.typedrouter.addTypedHandler( | ||||
|       new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_TriggerTask>( | ||||
|         'triggerTask', | ||||
|         async (reqArg) => { | ||||
|           await plugins.smartguard.passGuardsOrReject(reqArg, [ | ||||
|             this.cloudlyRef.authManager.validIdentityGuard, | ||||
|           ]); | ||||
|            | ||||
|           const execution = await this.executeTask( | ||||
|             reqArg.taskName, | ||||
|             'manual', | ||||
|             reqArg.userId | ||||
|           ); | ||||
|            | ||||
|           return { | ||||
|             execution: await execution.createSavableObject(), | ||||
|           }; | ||||
|         } | ||||
|       ) | ||||
|     ); | ||||
|      | ||||
|     // Cancel task | ||||
|     this.typedrouter.addTypedHandler( | ||||
|       new plugins.typedrequest.TypedHandler<plugins.servezoneInterfaces.requests.task.IRequest_Any_Cloudly_CancelTask>( | ||||
|         'cancelTask', | ||||
|         async (reqArg) => { | ||||
|           await plugins.smartguard.passGuardsOrReject(reqArg, [ | ||||
|             this.cloudlyRef.authManager.validIdentityGuard, | ||||
|           ]); | ||||
|            | ||||
|           const success = await this.cancelTask(reqArg.executionId); | ||||
|            | ||||
|           return { | ||||
|             success, | ||||
|           }; | ||||
|         } | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Initialize the task manager | ||||
|    */ | ||||
|   public async init() { | ||||
|     logger.log('info', 'Task Manager initialized'); | ||||
|      | ||||
|     // Clean up old executions on startup | ||||
|     const deletedCount = await TaskExecution.cleanupOldExecutions(30); | ||||
|     if (deletedCount > 0) { | ||||
|       logger.log('info', `Cleaned up ${deletedCount} old task executions`); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Stop the task manager | ||||
|    */ | ||||
|   public async stop() { | ||||
|     // Stop all scheduled tasks | ||||
|     await this.taskBufferManager.stop(); | ||||
|     logger.log('info', 'Task Manager stopped'); | ||||
|   } | ||||
| } | ||||
							
								
								
									
										432
									
								
								ts/manager.task/predefinedtasks.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										432
									
								
								ts/manager.task/predefinedtasks.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,432 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { CloudlyTaskManager } from './classes.taskmanager.js'; | ||||
| import { logger } from '../logger.js'; | ||||
|  | ||||
| /** | ||||
|  * Create and register all predefined tasks | ||||
|  */ | ||||
| export function createPredefinedTasks(taskManager: CloudlyTaskManager) { | ||||
|    | ||||
|   // DNS Sync Task | ||||
|   const dnsSync = new plugins.taskbuffer.Task({ | ||||
|     name: 'dns-sync', | ||||
|     taskFunction: async () => { | ||||
|       const execution = taskManager.getCurrentExecution('dns-sync'); | ||||
|       const dnsManager = taskManager.cloudlyRef.dnsManager; | ||||
|        | ||||
|       try { | ||||
|         await execution?.addLog('Starting DNS synchronization...', 'info'); | ||||
|          | ||||
|         // Get all DNS entries marked as external | ||||
|         const dnsEntries = await dnsManager.CDnsEntry.getInstances({ | ||||
|           'data.sourceType': 'external', | ||||
|         }); | ||||
|          | ||||
|         await execution?.addLog(`Found ${dnsEntries.length} external DNS entries to sync`, 'info'); | ||||
|         await execution?.setMetric('totalEntries', dnsEntries.length); | ||||
|          | ||||
|         let syncedCount = 0; | ||||
|         let failedCount = 0; | ||||
|          | ||||
|         for (const entry of dnsEntries) { | ||||
|           try { | ||||
|             // TODO: Implement actual sync with external DNS provider | ||||
|             await execution?.addLog(`Syncing DNS entry: ${entry.data.name}.${entry.data.zone}`, 'info'); | ||||
|             syncedCount++; | ||||
|           } catch (error) { | ||||
|             await execution?.addLog(`Failed to sync ${entry.data.name}: ${error.message}`, 'warning'); | ||||
|             failedCount++; | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         await execution?.setMetric('syncedCount', syncedCount); | ||||
|         await execution?.setMetric('failedCount', failedCount); | ||||
|         await execution?.addLog(`DNS sync completed: ${syncedCount} synced, ${failedCount} failed`, 'success'); | ||||
|          | ||||
|         return { synced: syncedCount, failed: failedCount }; | ||||
|       } catch (error) { | ||||
|         await execution?.addLog(`DNS sync error: ${error.message}`, 'error'); | ||||
|         throw error; | ||||
|       } | ||||
|     }, | ||||
|   }); | ||||
|    | ||||
|   taskManager.registerTask('dns-sync', dnsSync, { | ||||
|     description: 'Synchronize DNS entries with external providers', | ||||
|     category: 'system', | ||||
|     schedule: '0 */6 * * *', // Every 6 hours | ||||
|     enabled: true, | ||||
|   }); | ||||
|    | ||||
|   // Certificate Renewal Task | ||||
|   const certRenewal = new plugins.taskbuffer.Task({ | ||||
|     name: 'cert-renewal', | ||||
|     taskFunction: async () => { | ||||
|       const execution = taskManager.getCurrentExecution('cert-renewal'); | ||||
|        | ||||
|       try { | ||||
|         await execution?.addLog('Checking certificates for renewal...', 'info'); | ||||
|          | ||||
|         // Get all domains | ||||
|         const domains = await taskManager.cloudlyRef.domainManager.CDomain.getInstances({}); | ||||
|         await execution?.setMetric('totalDomains', domains.length); | ||||
|          | ||||
|         let renewedCount = 0; | ||||
|         let upToDateCount = 0; | ||||
|          | ||||
|         for (const domain of domains) { | ||||
|           // TODO: Check certificate expiry and renew if needed | ||||
|           await execution?.addLog(`Checking certificate for ${domain.data.name}`, 'info'); | ||||
|            | ||||
|           // Placeholder logic | ||||
|           const needsRenewal = Math.random() > 0.8; // 20% chance for demo | ||||
|            | ||||
|           if (needsRenewal) { | ||||
|             await execution?.addLog(`Renewing certificate for ${domain.data.name}`, 'info'); | ||||
|             // TODO: Actual renewal logic | ||||
|             renewedCount++; | ||||
|           } else { | ||||
|             upToDateCount++; | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         await execution?.setMetric('renewedCount', renewedCount); | ||||
|         await execution?.setMetric('upToDateCount', upToDateCount); | ||||
|         await execution?.addLog(`Certificate check completed: ${renewedCount} renewed, ${upToDateCount} up to date`, 'success'); | ||||
|          | ||||
|         return { renewed: renewedCount, upToDate: upToDateCount }; | ||||
|       } catch (error) { | ||||
|         await execution?.addLog(`Certificate renewal error: ${error.message}`, 'error'); | ||||
|         throw error; | ||||
|       } | ||||
|     }, | ||||
|   }); | ||||
|    | ||||
|   taskManager.registerTask('cert-renewal', certRenewal, { | ||||
|     description: 'Check and renew SSL certificates', | ||||
|     category: 'security', | ||||
|     schedule: '0 2 * * *', // Daily at 2 AM | ||||
|     enabled: true, | ||||
|   }); | ||||
|    | ||||
|   // Cleanup Task | ||||
|   const cleanup = new plugins.taskbuffer.Task({ | ||||
|     name: 'cleanup', | ||||
|     taskFunction: async () => { | ||||
|       const execution = taskManager.getCurrentExecution('cleanup'); | ||||
|        | ||||
|       try { | ||||
|         await execution?.addLog('Starting cleanup tasks...', 'info'); | ||||
|          | ||||
|         // Clean up old task executions | ||||
|         await execution?.addLog('Cleaning old task executions...', 'info'); | ||||
|         const deletedExecutions = await taskManager.CTaskExecution.cleanupOldExecutions(30); | ||||
|         await execution?.setMetric('deletedExecutions', deletedExecutions); | ||||
|          | ||||
|         // TODO: Clean up old logs | ||||
|         await execution?.addLog('Cleaning old logs...', 'info'); | ||||
|         // Placeholder | ||||
|         const deletedLogs = 0; | ||||
|         await execution?.setMetric('deletedLogs', deletedLogs); | ||||
|          | ||||
|         // TODO: Clean up Docker images | ||||
|         await execution?.addLog('Cleaning unused Docker images...', 'info'); | ||||
|         // Placeholder | ||||
|         const deletedImages = 0; | ||||
|         await execution?.setMetric('deletedImages', deletedImages); | ||||
|          | ||||
|         await execution?.addLog(`Cleanup completed: ${deletedExecutions} executions, ${deletedLogs} logs, ${deletedImages} images deleted`, 'success'); | ||||
|          | ||||
|         return {  | ||||
|           executions: deletedExecutions, | ||||
|           logs: deletedLogs, | ||||
|           images: deletedImages, | ||||
|         }; | ||||
|       } catch (error) { | ||||
|         await execution?.addLog(`Cleanup error: ${error.message}`, 'error'); | ||||
|         throw error; | ||||
|       } | ||||
|     }, | ||||
|   }); | ||||
|    | ||||
|   taskManager.registerTask('cleanup', cleanup, { | ||||
|     description: 'Remove old logs, executions, and temporary files', | ||||
|     category: 'cleanup', | ||||
|     schedule: '0 3 * * *', // Daily at 3 AM | ||||
|     enabled: true, | ||||
|   }); | ||||
|    | ||||
|   // Health Check Task | ||||
|   const healthCheck = new plugins.taskbuffer.Task({ | ||||
|     name: 'health-check', | ||||
|     taskFunction: async () => { | ||||
|       const execution = taskManager.getCurrentExecution('health-check'); | ||||
|        | ||||
|       try { | ||||
|         await execution?.addLog('Starting health checks...', 'info'); | ||||
|          | ||||
|         // Check all deployments | ||||
|         const deployments = await taskManager.cloudlyRef.deploymentManager.getAllDeployments(); | ||||
|         await execution?.setMetric('totalDeployments', deployments.length); | ||||
|          | ||||
|         let healthyCount = 0; | ||||
|         let unhealthyCount = 0; | ||||
|         const issues = []; | ||||
|          | ||||
|         for (const deployment of deployments) { | ||||
|           if (deployment.status === 'running') { | ||||
|             // TODO: Actual health check logic | ||||
|             const isHealthy = Math.random() > 0.1; // 90% healthy for demo | ||||
|              | ||||
|             if (isHealthy) { | ||||
|               healthyCount++; | ||||
|             } else { | ||||
|               unhealthyCount++; | ||||
|               issues.push({ | ||||
|                 deploymentId: deployment.id, | ||||
|                 serviceId: deployment.serviceId, | ||||
|                 issue: 'Health check failed', | ||||
|               }); | ||||
|               await execution?.addLog(`Deployment ${deployment.id} is unhealthy`, 'warning'); | ||||
|             } | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         await execution?.setMetric('healthyCount', healthyCount); | ||||
|         await execution?.setMetric('unhealthyCount', unhealthyCount); | ||||
|         await execution?.setMetric('issues', issues); | ||||
|          | ||||
|         const severity = unhealthyCount > 0 ? 'warning' : 'success'; | ||||
|         await execution?.addLog( | ||||
|           `Health check completed: ${healthyCount} healthy, ${unhealthyCount} unhealthy`, | ||||
|           severity as any | ||||
|         ); | ||||
|          | ||||
|         return { healthy: healthyCount, unhealthy: unhealthyCount, issues }; | ||||
|       } catch (error) { | ||||
|         await execution?.addLog(`Health check error: ${error.message}`, 'error'); | ||||
|         throw error; | ||||
|       } | ||||
|     }, | ||||
|   }); | ||||
|    | ||||
|   taskManager.registerTask('health-check', healthCheck, { | ||||
|     description: 'Monitor service health across deployments', | ||||
|     category: 'monitoring', | ||||
|     schedule: '*/15 * * * *', // Every 15 minutes | ||||
|     enabled: true, | ||||
|   }); | ||||
|    | ||||
|   // Resource Usage Report | ||||
|   const resourceReport = new plugins.taskbuffer.Task({ | ||||
|     name: 'resource-report', | ||||
|     taskFunction: async () => { | ||||
|       const execution = taskManager.getCurrentExecution('resource-report'); | ||||
|        | ||||
|       try { | ||||
|         await execution?.addLog('Generating resource usage report...', 'info'); | ||||
|          | ||||
|         // Get all nodes | ||||
|         const nodes = await taskManager.cloudlyRef.nodeManager.CClusterNode.getInstances({}); | ||||
|          | ||||
|         const report = { | ||||
|           timestamp: Date.now(), | ||||
|           nodes: [], | ||||
|           totalCpu: 0, | ||||
|           totalMemory: 0, | ||||
|           totalDisk: 0, | ||||
|         }; | ||||
|          | ||||
|         for (const node of nodes) { | ||||
|           // TODO: Get actual resource usage | ||||
|           const nodeUsage = { | ||||
|             nodeId: node.id, | ||||
|             nodeName: node.data.name, | ||||
|             cpu: Math.random() * 100, // Placeholder | ||||
|             memory: Math.random() * 100, // Placeholder | ||||
|             disk: Math.random() * 100, // Placeholder | ||||
|           }; | ||||
|            | ||||
|           report.nodes.push(nodeUsage); | ||||
|           report.totalCpu += nodeUsage.cpu; | ||||
|           report.totalMemory += nodeUsage.memory; | ||||
|           report.totalDisk += nodeUsage.disk; | ||||
|         } | ||||
|          | ||||
|         // Calculate averages | ||||
|         if (nodes.length > 0) { | ||||
|           report.totalCpu /= nodes.length; | ||||
|           report.totalMemory /= nodes.length; | ||||
|           report.totalDisk /= nodes.length; | ||||
|         } | ||||
|          | ||||
|         await execution?.setMetric('report', report); | ||||
|         await execution?.addLog( | ||||
|           `Resource report generated: Avg CPU ${report.totalCpu.toFixed(1)}%, Memory ${report.totalMemory.toFixed(1)}%, Disk ${report.totalDisk.toFixed(1)}%`, | ||||
|           'success' | ||||
|         ); | ||||
|          | ||||
|         return report; | ||||
|       } catch (error) { | ||||
|         await execution?.addLog(`Resource report error: ${error.message}`, 'error'); | ||||
|         throw error; | ||||
|       } | ||||
|     }, | ||||
|   }); | ||||
|    | ||||
|   taskManager.registerTask('resource-report', resourceReport, { | ||||
|     description: 'Generate resource usage reports', | ||||
|     category: 'monitoring', | ||||
|     schedule: '0 * * * *', // Every hour | ||||
|     enabled: true, | ||||
|   }); | ||||
|    | ||||
|   // Database Maintenance | ||||
|   const dbMaintenance = new plugins.taskbuffer.Task({ | ||||
|     name: 'db-maintenance', | ||||
|     taskFunction: async () => { | ||||
|       const execution = taskManager.getCurrentExecution('db-maintenance'); | ||||
|        | ||||
|       try { | ||||
|         await execution?.addLog('Starting database maintenance...', 'info'); | ||||
|          | ||||
|         // TODO: Implement actual database maintenance | ||||
|         await execution?.addLog('Analyzing indexes...', 'info'); | ||||
|         await execution?.addLog('Compacting collections...', 'info'); | ||||
|         await execution?.addLog('Updating statistics...', 'info'); | ||||
|          | ||||
|         await execution?.setMetric('collectionsOptimized', 5); // Placeholder | ||||
|         await execution?.setMetric('indexesRebuilt', 3); // Placeholder | ||||
|          | ||||
|         await execution?.addLog('Database maintenance completed', 'success'); | ||||
|          | ||||
|         return { success: true }; | ||||
|       } catch (error) { | ||||
|         await execution?.addLog(`Database maintenance error: ${error.message}`, 'error'); | ||||
|         throw error; | ||||
|       } | ||||
|     }, | ||||
|   }); | ||||
|    | ||||
|   taskManager.registerTask('db-maintenance', dbMaintenance, { | ||||
|     description: 'Optimize database performance', | ||||
|     category: 'maintenance', | ||||
|     schedule: '0 4 * * 0', // Weekly on Sunday at 4 AM | ||||
|     enabled: true, | ||||
|   }); | ||||
|    | ||||
|   // Security Scan | ||||
|   const securityScan = new plugins.taskbuffer.Task({ | ||||
|     name: 'security-scan', | ||||
|     taskFunction: async () => { | ||||
|       const execution = taskManager.getCurrentExecution('security-scan'); | ||||
|        | ||||
|       try { | ||||
|         await execution?.addLog('Starting security scan...', 'info'); | ||||
|          | ||||
|         const vulnerabilities = []; | ||||
|          | ||||
|         // Check for exposed ports | ||||
|         await execution?.addLog('Checking for exposed ports...', 'info'); | ||||
|         // TODO: Actual port scanning logic | ||||
|          | ||||
|         // Check for outdated images | ||||
|         await execution?.addLog('Checking for outdated images...', 'info'); | ||||
|         const images = await taskManager.cloudlyRef.imageManager.CImage.getInstances({}); | ||||
|          | ||||
|         for (const image of images) { | ||||
|           // TODO: Check if image is outdated | ||||
|           const isOutdated = Math.random() > 0.7; // 30% outdated for demo | ||||
|            | ||||
|           if (isOutdated) { | ||||
|             vulnerabilities.push({ | ||||
|               type: 'outdated-image', | ||||
|               severity: 'medium', | ||||
|               image: image.data.name, | ||||
|               version: image.data.version, | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|          | ||||
|         // Check for weak passwords | ||||
|         await execution?.addLog('Checking for weak configurations...', 'info'); | ||||
|         // TODO: Configuration checks | ||||
|          | ||||
|         await execution?.setMetric('vulnerabilitiesFound', vulnerabilities.length); | ||||
|         await execution?.setMetric('vulnerabilities', vulnerabilities); | ||||
|          | ||||
|         const severity = vulnerabilities.length > 0 ? 'warning' : 'success'; | ||||
|         await execution?.addLog( | ||||
|           `Security scan completed: ${vulnerabilities.length} vulnerabilities found`, | ||||
|           severity as any | ||||
|         ); | ||||
|          | ||||
|         return { vulnerabilities }; | ||||
|       } catch (error) { | ||||
|         await execution?.addLog(`Security scan error: ${error.message}`, 'error'); | ||||
|         throw error; | ||||
|       } | ||||
|     }, | ||||
|   }); | ||||
|    | ||||
|   taskManager.registerTask('security-scan', securityScan, { | ||||
|     description: 'Run security checks on services', | ||||
|     category: 'security', | ||||
|     schedule: '0 1 * * *', // Daily at 1 AM | ||||
|     enabled: true, | ||||
|   }); | ||||
|    | ||||
|   // Docker Cleanup | ||||
|   const dockerCleanup = new plugins.taskbuffer.Task({ | ||||
|     name: 'docker-cleanup', | ||||
|     taskFunction: async () => { | ||||
|       const execution = taskManager.getCurrentExecution('docker-cleanup'); | ||||
|        | ||||
|       try { | ||||
|         await execution?.addLog('Starting Docker cleanup...', 'info'); | ||||
|          | ||||
|         // TODO: Implement actual Docker cleanup | ||||
|         await execution?.addLog('Removing stopped containers...', 'info'); | ||||
|         const removedContainers = 0; // Placeholder | ||||
|          | ||||
|         await execution?.addLog('Removing unused images...', 'info'); | ||||
|         const removedImages = 0; // Placeholder | ||||
|          | ||||
|         await execution?.addLog('Removing unused volumes...', 'info'); | ||||
|         const removedVolumes = 0; // Placeholder | ||||
|          | ||||
|         await execution?.addLog('Removing unused networks...', 'info'); | ||||
|         const removedNetworks = 0; // Placeholder | ||||
|          | ||||
|         await execution?.setMetric('removedContainers', removedContainers); | ||||
|         await execution?.setMetric('removedImages', removedImages); | ||||
|         await execution?.setMetric('removedVolumes', removedVolumes); | ||||
|         await execution?.setMetric('removedNetworks', removedNetworks); | ||||
|          | ||||
|         await execution?.addLog( | ||||
|           `Docker cleanup completed: ${removedContainers} containers, ${removedImages} images, ${removedVolumes} volumes, ${removedNetworks} networks removed`, | ||||
|           'success' | ||||
|         ); | ||||
|          | ||||
|         return { | ||||
|           containers: removedContainers, | ||||
|           images: removedImages, | ||||
|           volumes: removedVolumes, | ||||
|           networks: removedNetworks, | ||||
|         }; | ||||
|       } catch (error) { | ||||
|         await execution?.addLog(`Docker cleanup error: ${error.message}`, 'error'); | ||||
|         throw error; | ||||
|       } | ||||
|     }, | ||||
|   }); | ||||
|    | ||||
|   taskManager.registerTask('docker-cleanup', dockerCleanup, { | ||||
|     description: 'Remove unused Docker images and containers', | ||||
|     category: 'cleanup', | ||||
|     schedule: '0 5 * * *', // Daily at 5 AM | ||||
|     enabled: true, | ||||
|   }); | ||||
|    | ||||
|   logger.log('info', 'Predefined tasks registered successfully'); | ||||
| } | ||||
| @@ -1,35 +0,0 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import { Cloudly } from '../classes.cloudly.js'; | ||||
|  | ||||
| import { logger } from '../logger.js'; | ||||
|  | ||||
| export class CloudlyTaskmanager { | ||||
|   public cloudlyRef: Cloudly; | ||||
|  | ||||
|   constructor(cloudlyRefArg: Cloudly) { | ||||
|     this.cloudlyRef = cloudlyRefArg; | ||||
|   } | ||||
|  | ||||
|   public everyMinuteTask = new plugins.taskbuffer.Task({ | ||||
|     taskFunction: async () => {}, | ||||
|   }); | ||||
|  | ||||
|   public everyHourTask = new plugins.taskbuffer.Task({ | ||||
|     taskFunction: async () => { | ||||
|       logger.log('info', `Performing hourly maintenance check.`); | ||||
|       const configs = await this.cloudlyRef.clusterManager.getAllClusters(); | ||||
|       logger.log('info', `Got ${configs.length} configs`); | ||||
|       configs.map((configArg) => { | ||||
|         console.log(configArg.name); | ||||
|       }); | ||||
|     }, | ||||
|   }); | ||||
|  | ||||
|   public everyDayTask = new plugins.taskbuffer.Task({ | ||||
|     taskFunction: async () => {}, | ||||
|   }); | ||||
|  | ||||
|   public everyWeekTask = new plugins.taskbuffer.Task({ | ||||
|     taskFunction: async () => {}, | ||||
|   }); | ||||
| } | ||||
| @@ -15,6 +15,7 @@ export * from './clusternode.js'; | ||||
| export * from './settings.js'; | ||||
| export * from './service.js'; | ||||
| export * from './status.js'; | ||||
| export * from './taskexecution.js'; | ||||
| export * from './traffic.js'; | ||||
| export * from './user.js'; | ||||
| export * from './version.js'; | ||||
|   | ||||
							
								
								
									
										84
									
								
								ts_interfaces/data/taskexecution.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										84
									
								
								ts_interfaces/data/taskexecution.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,84 @@ | ||||
| /** | ||||
|  * Task execution tracking for the task management system | ||||
|  * Tasks themselves are hard-coded using @push.rocks/taskbuffer | ||||
|  * This interface tracks execution history and outcomes | ||||
|  */ | ||||
| export interface ITaskExecution { | ||||
|   id: string; | ||||
|   data: { | ||||
|     /** | ||||
|      * Name of the task being executed | ||||
|      */ | ||||
|     taskName: string; | ||||
|      | ||||
|     /** | ||||
|      * Optional description of what the task does | ||||
|      */ | ||||
|     taskDescription?: string; | ||||
|      | ||||
|     /** | ||||
|      * Category for grouping tasks | ||||
|      */ | ||||
|     category?: 'maintenance' | 'deployment' | 'backup' | 'monitoring' | 'cleanup' | 'system' | 'security'; | ||||
|      | ||||
|     /** | ||||
|      * Timestamp when the task started | ||||
|      */ | ||||
|     startedAt: number; | ||||
|      | ||||
|     /** | ||||
|      * Timestamp when the task completed | ||||
|      */ | ||||
|     completedAt?: number; | ||||
|      | ||||
|     /** | ||||
|      * Current status of the task execution | ||||
|      */ | ||||
|     status: 'running' | 'completed' | 'failed' | 'cancelled'; | ||||
|      | ||||
|     /** | ||||
|      * Duration in milliseconds | ||||
|      */ | ||||
|     duration?: number; | ||||
|      | ||||
|     /** | ||||
|      * How the task was triggered | ||||
|      */ | ||||
|     triggeredBy: 'schedule' | 'manual' | 'system'; | ||||
|      | ||||
|     /** | ||||
|      * User ID if manually triggered | ||||
|      */ | ||||
|     userId?: string; | ||||
|      | ||||
|     /** | ||||
|      * Execution logs | ||||
|      */ | ||||
|     logs: Array<{ | ||||
|       timestamp: number; | ||||
|       message: string; | ||||
|       severity: 'info' | 'warning' | 'error' | 'success'; | ||||
|     }>; | ||||
|      | ||||
|     /** | ||||
|      * Task-specific metrics | ||||
|      */ | ||||
|     metrics?: { | ||||
|       [key: string]: any; | ||||
|     }; | ||||
|      | ||||
|     /** | ||||
|      * Final result/output of the task | ||||
|      */ | ||||
|     result?: any; | ||||
|      | ||||
|     /** | ||||
|      * Error details if the task failed | ||||
|      */ | ||||
|     error?: { | ||||
|       message: string; | ||||
|       stack?: string; | ||||
|       code?: string; | ||||
|     }; | ||||
|   }; | ||||
| } | ||||
| @@ -22,6 +22,7 @@ import * as serverRequests from './server.js'; | ||||
| import * as serviceRequests from './service.js'; | ||||
| import * as settingsRequests from './settings.js'; | ||||
| import * as statusRequests from './status.js'; | ||||
| import * as taskRequests from './task.js'; | ||||
| import * as versionRequests from './version.js'; | ||||
|  | ||||
| export { | ||||
| @@ -47,6 +48,7 @@ export { | ||||
|   serviceRequests as service, | ||||
|   settingsRequests as settings, | ||||
|   statusRequests as status, | ||||
|   taskRequests as task, | ||||
|   versionRequests as version, | ||||
| }; | ||||
|  | ||||
|   | ||||
							
								
								
									
										88
									
								
								ts_interfaces/requests/task.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										88
									
								
								ts_interfaces/requests/task.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,88 @@ | ||||
| import * as plugins from '../plugins.js'; | ||||
| import * as data from '../data/index.js'; | ||||
|  | ||||
| // Get all tasks | ||||
| export interface IRequest_Any_Cloudly_GetTasks | ||||
|   extends plugins.typedrequestInterfaces.implementsTR< | ||||
|     plugins.typedrequestInterfaces.ITypedRequest, | ||||
|     IRequest_Any_Cloudly_GetTasks | ||||
|   > { | ||||
|   method: 'getTasks'; | ||||
|   request: {}; | ||||
|   response: { | ||||
|     tasks: Array<{ | ||||
|       name: string; | ||||
|       description: string; | ||||
|       category: 'maintenance' | 'deployment' | 'backup' | 'monitoring' | 'cleanup' | 'system' | 'security'; | ||||
|       schedule?: string; | ||||
|       lastRun?: number; | ||||
|       enabled: boolean; | ||||
|     }>; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| // Get task executions | ||||
| export interface IRequest_Any_Cloudly_GetTaskExecutions | ||||
|   extends plugins.typedrequestInterfaces.implementsTR< | ||||
|     plugins.typedrequestInterfaces.ITypedRequest, | ||||
|     IRequest_Any_Cloudly_GetTaskExecutions | ||||
|   > { | ||||
|   method: 'getTaskExecutions'; | ||||
|   request: { | ||||
|     filter?: { | ||||
|       taskName?: string; | ||||
|       status?: string; | ||||
|       startedAfter?: number; | ||||
|       startedBefore?: number; | ||||
|     }; | ||||
|   }; | ||||
|   response: { | ||||
|     executions: data.ITaskExecution[]; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| // Get task execution by ID | ||||
| export interface IRequest_Any_Cloudly_GetTaskExecutionById | ||||
|   extends plugins.typedrequestInterfaces.implementsTR< | ||||
|     plugins.typedrequestInterfaces.ITypedRequest, | ||||
|     IRequest_Any_Cloudly_GetTaskExecutionById | ||||
|   > { | ||||
|   method: 'getTaskExecutionById'; | ||||
|   request: { | ||||
|     executionId: string; | ||||
|   }; | ||||
|   response: { | ||||
|     execution: data.ITaskExecution; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| // Trigger task manually | ||||
| export interface IRequest_Any_Cloudly_TriggerTask | ||||
|   extends plugins.typedrequestInterfaces.implementsTR< | ||||
|     plugins.typedrequestInterfaces.ITypedRequest, | ||||
|     IRequest_Any_Cloudly_TriggerTask | ||||
|   > { | ||||
|   method: 'triggerTask'; | ||||
|   request: { | ||||
|     taskName: string; | ||||
|     userId?: string; | ||||
|   }; | ||||
|   response: { | ||||
|     execution: data.ITaskExecution; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| // Cancel a running task | ||||
| export interface IRequest_Any_Cloudly_CancelTask | ||||
|   extends plugins.typedrequestInterfaces.implementsTR< | ||||
|     plugins.typedrequestInterfaces.ITypedRequest, | ||||
|     IRequest_Any_Cloudly_CancelTask | ||||
|   > { | ||||
|   method: 'cancelTask'; | ||||
|   request: { | ||||
|     executionId: string; | ||||
|   }; | ||||
|   response: { | ||||
|     success: boolean; | ||||
|   }; | ||||
| } | ||||
| @@ -663,6 +663,89 @@ export const verifyExternalRegistryAction = dataState.createAction( | ||||
|   } | ||||
| ); | ||||
|  | ||||
| // Task Actions | ||||
| export const taskActions = { | ||||
|   getTasks: dataState.createAction( | ||||
|     async (statePartArg, payloadArg: {}) => { | ||||
|       const currentState = statePartArg.getState(); | ||||
|       const trGetTasks = | ||||
|         new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.task.IRequest_Any_Cloudly_GetTasks>( | ||||
|           '/typedrequest', | ||||
|           'getTasks' | ||||
|         ); | ||||
|       const response = await trGetTasks.fire({ | ||||
|         identity: loginStatePart.getState().identity, | ||||
|       }); | ||||
|       return response as any; | ||||
|     } | ||||
|   ), | ||||
|    | ||||
|   getTaskExecutions: dataState.createAction( | ||||
|     async (statePartArg, payloadArg: { filter?: any }) => { | ||||
|       const currentState = statePartArg.getState(); | ||||
|       const trGetTaskExecutions = | ||||
|         new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.task.IRequest_Any_Cloudly_GetTaskExecutions>( | ||||
|           '/typedrequest', | ||||
|           'getTaskExecutions' | ||||
|         ); | ||||
|       const response = await trGetTaskExecutions.fire({ | ||||
|         identity: loginStatePart.getState().identity, | ||||
|         filter: payloadArg.filter, | ||||
|       }); | ||||
|       return response as any; | ||||
|     } | ||||
|   ), | ||||
|    | ||||
|   getTaskExecutionById: dataState.createAction( | ||||
|     async (statePartArg, payloadArg: { executionId: string }) => { | ||||
|       const currentState = statePartArg.getState(); | ||||
|       const trGetTaskExecutionById = | ||||
|         new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.task.IRequest_Any_Cloudly_GetTaskExecutionById>( | ||||
|           '/typedrequest', | ||||
|           'getTaskExecutionById' | ||||
|         ); | ||||
|       const response = await trGetTaskExecutionById.fire({ | ||||
|         identity: loginStatePart.getState().identity, | ||||
|         executionId: payloadArg.executionId, | ||||
|       }); | ||||
|       return response as any; | ||||
|     } | ||||
|   ), | ||||
|    | ||||
|   triggerTask: dataState.createAction( | ||||
|     async (statePartArg, payloadArg: { taskName: string; userId?: string }) => { | ||||
|       const currentState = statePartArg.getState(); | ||||
|       const trTriggerTask = | ||||
|         new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.task.IRequest_Any_Cloudly_TriggerTask>( | ||||
|           '/typedrequest', | ||||
|           'triggerTask' | ||||
|         ); | ||||
|       const response = await trTriggerTask.fire({ | ||||
|         identity: loginStatePart.getState().identity, | ||||
|         taskName: payloadArg.taskName, | ||||
|         userId: payloadArg.userId, | ||||
|       }); | ||||
|       return currentState; | ||||
|     } | ||||
|   ), | ||||
|    | ||||
|   cancelTask: dataState.createAction( | ||||
|     async (statePartArg, payloadArg: { executionId: string }) => { | ||||
|       const currentState = statePartArg.getState(); | ||||
|       const trCancelTask = | ||||
|         new domtools.plugins.typedrequest.TypedRequest<plugins.interfaces.requests.task.IRequest_Any_Cloudly_CancelTask>( | ||||
|           '/typedrequest', | ||||
|           'cancelTask' | ||||
|         ); | ||||
|       const response = await trCancelTask.fire({ | ||||
|         identity: loginStatePart.getState().identity, | ||||
|         executionId: payloadArg.executionId, | ||||
|       }); | ||||
|       return currentState; | ||||
|     } | ||||
|   ), | ||||
| }; | ||||
|  | ||||
| // cluster | ||||
| export const addClusterAction = dataState.createAction( | ||||
|   async ( | ||||
|   | ||||
| @@ -27,6 +27,7 @@ import { CloudlyViewSecretGroups } from './cloudly-view-secretgroups.js'; | ||||
| import { CloudlyViewServices } from './cloudly-view-services.js'; | ||||
| import { CloudlyViewExternalRegistries } from './cloudly-view-externalregistries.js'; | ||||
| import { CloudlyViewSettings } from './cloudly-view-settings.js'; | ||||
| import { CloudlyViewTasks } from './cloudly-view-tasks.js'; | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
| @@ -126,6 +127,11 @@ export class CloudlyDashboard extends DeesElement { | ||||
|                 iconName: 'lucide:Rocket', | ||||
|                 element: CloudlyViewDeployments, | ||||
|               }, | ||||
|               { | ||||
|                 name: 'Tasks', | ||||
|                 iconName: 'lucide:ListChecks', | ||||
|                 element: CloudlyViewTasks, | ||||
|               }, | ||||
|               { | ||||
|                 name: 'Domains', | ||||
|                 iconName: 'lucide:Globe2', | ||||
|   | ||||
							
								
								
									
										560
									
								
								ts_web/elements/cloudly-view-tasks.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										560
									
								
								ts_web/elements/cloudly-view-tasks.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,560 @@ | ||||
| import * as shared from '../elements/shared/index.js'; | ||||
| import * as plugins from '../plugins.js'; | ||||
|  | ||||
| import { | ||||
|   DeesElement, | ||||
|   customElement, | ||||
|   html, | ||||
|   state, | ||||
|   css, | ||||
|   cssManager, | ||||
|   property, | ||||
| } from '@design.estate/dees-element'; | ||||
|  | ||||
| import * as appstate from '../appstate.js'; | ||||
|  | ||||
| @customElement('cloudly-view-tasks') | ||||
| export class CloudlyViewTasks extends DeesElement { | ||||
|   @state() | ||||
|   private data: appstate.IDataState = {}; | ||||
|  | ||||
|   @state() | ||||
|   private tasks: any[] = []; | ||||
|  | ||||
|   @state() | ||||
|   private executions: plugins.interfaces.data.ITaskExecution[] = []; | ||||
|  | ||||
|   @state() | ||||
|   private selectedExecution: plugins.interfaces.data.ITaskExecution | null = null; | ||||
|  | ||||
|   @state() | ||||
|   private loading = false; | ||||
|  | ||||
|   @state() | ||||
|   private filterStatus: string = 'all'; | ||||
|  | ||||
|   constructor() { | ||||
|     super(); | ||||
|     const subscription = appstate.dataState | ||||
|       .select((stateArg) => stateArg) | ||||
|       .subscribe((dataArg) => { | ||||
|         this.data = dataArg; | ||||
|       }); | ||||
|     this.rxSubscriptions.push(subscription); | ||||
|   } | ||||
|  | ||||
|   public static styles = [ | ||||
|     cssManager.defaultStyles, | ||||
|     shared.viewHostCss, | ||||
|     css` | ||||
|       .task-grid { | ||||
|         display: grid; | ||||
|         grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); | ||||
|         gap: 16px; | ||||
|         margin-bottom: 32px; | ||||
|       } | ||||
|  | ||||
|       .task-card { | ||||
|         background: #1a1a1a; | ||||
|         border: 1px solid #333; | ||||
|         border-radius: 8px; | ||||
|         padding: 16px; | ||||
|         cursor: pointer; | ||||
|         transition: all 0.2s; | ||||
|       } | ||||
|  | ||||
|       .task-card:hover { | ||||
|         background: #222; | ||||
|         border-color: #555; | ||||
|       } | ||||
|  | ||||
|       .task-header { | ||||
|         display: flex; | ||||
|         justify-content: space-between; | ||||
|         align-items: center; | ||||
|         margin-bottom: 12px; | ||||
|       } | ||||
|  | ||||
|       .task-name { | ||||
|         font-size: 1.1em; | ||||
|         font-weight: 600; | ||||
|         color: #fff; | ||||
|       } | ||||
|  | ||||
|       .task-description { | ||||
|         color: #999; | ||||
|         font-size: 0.9em; | ||||
|         margin-bottom: 12px; | ||||
|       } | ||||
|  | ||||
|       .task-meta { | ||||
|         display: flex; | ||||
|         gap: 8px; | ||||
|         flex-wrap: wrap; | ||||
|       } | ||||
|  | ||||
|       .category-badge, .status-badge { | ||||
|         padding: 2px 8px; | ||||
|         border-radius: 4px; | ||||
|         font-size: 0.85em; | ||||
|         font-weight: 500; | ||||
|       } | ||||
|  | ||||
|       .category-maintenance { | ||||
|         background: #ff9800; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .category-deployment { | ||||
|         background: #2196f3; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .category-backup { | ||||
|         background: #4caf50; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .category-monitoring { | ||||
|         background: #9c27b0; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .category-cleanup { | ||||
|         background: #795548; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .category-system { | ||||
|         background: #607d8b; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .category-security { | ||||
|         background: #f44336; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .status-running { | ||||
|         background: #2196f3; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .status-completed { | ||||
|         background: #4caf50; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .status-failed { | ||||
|         background: #f44336; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .status-cancelled { | ||||
|         background: #ff9800; | ||||
|         color: white; | ||||
|       } | ||||
|  | ||||
|       .trigger-button { | ||||
|         padding: 6px 12px; | ||||
|         background: #2196f3; | ||||
|         color: white; | ||||
|         border: none; | ||||
|         border-radius: 4px; | ||||
|         cursor: pointer; | ||||
|         font-size: 0.9em; | ||||
|         transition: background 0.2s; | ||||
|       } | ||||
|  | ||||
|       .trigger-button:hover { | ||||
|         background: #1976d2; | ||||
|       } | ||||
|  | ||||
|       .trigger-button:disabled { | ||||
|         background: #666; | ||||
|         cursor: not-allowed; | ||||
|       } | ||||
|  | ||||
|       .schedule-info { | ||||
|         color: #666; | ||||
|         font-size: 0.85em; | ||||
|         margin-top: 8px; | ||||
|       } | ||||
|  | ||||
|       .last-run { | ||||
|         color: #888; | ||||
|         font-size: 0.85em; | ||||
|         margin-top: 4px; | ||||
|       } | ||||
|  | ||||
|       .execution-logs { | ||||
|         background: #0a0a0a; | ||||
|         border: 1px solid #333; | ||||
|         border-radius: 4px; | ||||
|         padding: 16px; | ||||
|         margin-top: 16px; | ||||
|         max-height: 400px; | ||||
|         overflow-y: auto; | ||||
|       } | ||||
|  | ||||
|       .log-entry { | ||||
|         font-family: monospace; | ||||
|         font-size: 0.9em; | ||||
|         margin-bottom: 8px; | ||||
|         padding: 4px 8px; | ||||
|         border-radius: 4px; | ||||
|       } | ||||
|  | ||||
|       .log-info { | ||||
|         color: #2196f3; | ||||
|       } | ||||
|  | ||||
|       .log-warning { | ||||
|         color: #ff9800; | ||||
|         background: rgba(255, 152, 0, 0.1); | ||||
|       } | ||||
|  | ||||
|       .log-error { | ||||
|         color: #f44336; | ||||
|         background: rgba(244, 67, 54, 0.1); | ||||
|       } | ||||
|  | ||||
|       .log-success { | ||||
|         color: #4caf50; | ||||
|         background: rgba(76, 175, 80, 0.1); | ||||
|       } | ||||
|  | ||||
|       .filter-bar { | ||||
|         display: flex; | ||||
|         gap: 8px; | ||||
|         margin-bottom: 16px; | ||||
|       } | ||||
|  | ||||
|       .filter-button { | ||||
|         padding: 6px 12px; | ||||
|         background: #333; | ||||
|         color: #ccc; | ||||
|         border: 1px solid #555; | ||||
|         border-radius: 4px; | ||||
|         cursor: pointer; | ||||
|         transition: all 0.2s; | ||||
|       } | ||||
|  | ||||
|       .filter-button.active { | ||||
|         background: #2196f3; | ||||
|         color: white; | ||||
|         border-color: #2196f3; | ||||
|       } | ||||
|  | ||||
|       .filter-button:hover:not(.active) { | ||||
|         background: #444; | ||||
|       } | ||||
|  | ||||
|       .metrics { | ||||
|         display: flex; | ||||
|         gap: 16px; | ||||
|         margin-top: 12px; | ||||
|         padding-top: 12px; | ||||
|         border-top: 1px solid #333; | ||||
|       } | ||||
|  | ||||
|       .metric { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|       } | ||||
|  | ||||
|       .metric-label { | ||||
|         color: #666; | ||||
|         font-size: 0.85em; | ||||
|       } | ||||
|  | ||||
|       .metric-value { | ||||
|         color: #fff; | ||||
|         font-size: 1.1em; | ||||
|         font-weight: 600; | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
|  | ||||
|   async connectedCallback() { | ||||
|     super.connectedCallback(); | ||||
|     await this.loadTasks(); | ||||
|     await this.loadExecutions(); | ||||
|   } | ||||
|  | ||||
|   private async loadTasks() { | ||||
|     this.loading = true; | ||||
|     try { | ||||
|       const response: any = await appstate.dataState.dispatchAction( | ||||
|         appstate.taskActions.getTasks, {} | ||||
|       ); | ||||
|       this.tasks = response.tasks || []; | ||||
|     } catch (error) { | ||||
|       console.error('Failed to load tasks:', error); | ||||
|     } finally { | ||||
|       this.loading = false; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async loadExecutions() { | ||||
|     try { | ||||
|       const filter: any = {}; | ||||
|       if (this.filterStatus !== 'all') { | ||||
|         filter.status = this.filterStatus; | ||||
|       } | ||||
|        | ||||
|       const response: any = await appstate.dataState.dispatchAction( | ||||
|         appstate.taskActions.getTaskExecutions, { filter } | ||||
|       ); | ||||
|       this.executions = response.executions || []; | ||||
|     } catch (error) { | ||||
|       console.error('Failed to load executions:', error); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async triggerTask(taskName: string) { | ||||
|     try { | ||||
|       await appstate.dataState.dispatchAction( | ||||
|         appstate.taskActions.triggerTask, { taskName } | ||||
|       ); | ||||
|        | ||||
|       // Reload tasks and executions to show the new execution | ||||
|       await this.loadTasks(); | ||||
|       await this.loadExecutions(); | ||||
|     } catch (error) { | ||||
|       console.error('Failed to trigger task:', error); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private formatDate(timestamp: number): string { | ||||
|     return new Date(timestamp).toLocaleString(); | ||||
|   } | ||||
|  | ||||
|   private formatDuration(ms: number): string { | ||||
|     if (ms < 1000) return `${ms}ms`; | ||||
|     if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; | ||||
|     if (ms < 3600000) return `${(ms / 60000).toFixed(1)}m`; | ||||
|     return `${(ms / 3600000).toFixed(1)}h`; | ||||
|   } | ||||
|  | ||||
|   private getCategoryIcon(category: string): string { | ||||
|     switch (category) { | ||||
|       case 'maintenance': | ||||
|         return 'lucide:Wrench'; | ||||
|       case 'deployment': | ||||
|         return 'lucide:Rocket'; | ||||
|       case 'backup': | ||||
|         return 'lucide:Archive'; | ||||
|       case 'monitoring': | ||||
|         return 'lucide:Activity'; | ||||
|       case 'cleanup': | ||||
|         return 'lucide:Trash2'; | ||||
|       case 'system': | ||||
|         return 'lucide:Settings'; | ||||
|       case 'security': | ||||
|         return 'lucide:Shield'; | ||||
|       default: | ||||
|         return 'lucide:Play'; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private renderTaskCard(task: any) { | ||||
|     const lastExecution = this.executions | ||||
|       .filter(e => e.data.taskName === task.name) | ||||
|       .sort((a, b) => (b.data.startedAt || 0) - (a.data.startedAt || 0))[0]; | ||||
|      | ||||
|     const isRunning = lastExecution?.data.status === 'running'; | ||||
|      | ||||
|     return html` | ||||
|       <div class="task-card"> | ||||
|         <div class="task-header"> | ||||
|           <div class="task-name"> | ||||
|             <dees-icon .iconName=${this.getCategoryIcon(task.category)}></dees-icon> | ||||
|             ${task.name} | ||||
|           </div> | ||||
|           <button | ||||
|             class="trigger-button" | ||||
|             ?disabled=${isRunning || !task.enabled} | ||||
|             @click=${() => this.triggerTask(task.name)} | ||||
|           > | ||||
|             ${isRunning ? 'Running...' : 'Run'} | ||||
|           </button> | ||||
|         </div> | ||||
|          | ||||
|         <div class="task-description">${task.description}</div> | ||||
|          | ||||
|         <div class="task-meta"> | ||||
|           <span class="category-badge category-${task.category}">${task.category}</span> | ||||
|           ${!task.enabled ? html`<span class="status-badge status-cancelled">Disabled</span>` : ''} | ||||
|         </div> | ||||
|          | ||||
|         ${task.schedule ? html` | ||||
|           <div class="schedule-info"> | ||||
|             <dees-icon .iconName=${'lucide:Clock'}></dees-icon> | ||||
|             Schedule: ${task.schedule} | ||||
|           </div> | ||||
|         ` : ''} | ||||
|          | ||||
|         ${task.lastRun ? html` | ||||
|           <div class="last-run"> | ||||
|             Last run: ${this.formatDate(task.lastRun)} | ||||
|           </div> | ||||
|         ` : ''} | ||||
|          | ||||
|         ${lastExecution ? html` | ||||
|           <div class="metrics"> | ||||
|             <div class="metric"> | ||||
|               <span class="metric-label">Status</span> | ||||
|               <span class="metric-value"> | ||||
|                 <span class="status-badge status-${lastExecution.data.status}"> | ||||
|                   ${lastExecution.data.status} | ||||
|                 </span> | ||||
|               </span> | ||||
|             </div> | ||||
|             ${lastExecution.data.duration ? html` | ||||
|               <div class="metric"> | ||||
|                 <span class="metric-label">Duration</span> | ||||
|                 <span class="metric-value">${this.formatDuration(lastExecution.data.duration)}</span> | ||||
|               </div> | ||||
|             ` : ''} | ||||
|           </div> | ||||
|         ` : ''} | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private renderExecutionDetails(execution: plugins.interfaces.data.ITaskExecution) { | ||||
|     return html` | ||||
|       <div class="execution-details"> | ||||
|         <h3>Execution Details: ${execution.data.taskName}</h3> | ||||
|          | ||||
|         <div class="metrics"> | ||||
|           <div class="metric"> | ||||
|             <span class="metric-label">Started</span> | ||||
|             <span class="metric-value">${this.formatDate(execution.data.startedAt)}</span> | ||||
|           </div> | ||||
|           ${execution.data.completedAt ? html` | ||||
|             <div class="metric"> | ||||
|               <span class="metric-label">Completed</span> | ||||
|               <span class="metric-value">${this.formatDate(execution.data.completedAt)}</span> | ||||
|             </div> | ||||
|           ` : ''} | ||||
|           ${execution.data.duration ? html` | ||||
|             <div class="metric"> | ||||
|               <span class="metric-label">Duration</span> | ||||
|               <span class="metric-value">${this.formatDuration(execution.data.duration)}</span> | ||||
|             </div> | ||||
|           ` : ''} | ||||
|           <div class="metric"> | ||||
|             <span class="metric-label">Triggered By</span> | ||||
|             <span class="metric-value">${execution.data.triggeredBy}</span> | ||||
|           </div> | ||||
|         </div> | ||||
|          | ||||
|         ${execution.data.logs && execution.data.logs.length > 0 ? html` | ||||
|           <h4>Logs</h4> | ||||
|           <div class="execution-logs"> | ||||
|             ${execution.data.logs.map(log => html` | ||||
|               <div class="log-entry log-${log.severity}"> | ||||
|                 <span>${this.formatDate(log.timestamp)}</span> -  | ||||
|                 ${log.message} | ||||
|               </div> | ||||
|             `)} | ||||
|           </div> | ||||
|         ` : ''} | ||||
|          | ||||
|         ${execution.data.metrics ? html` | ||||
|           <h4>Metrics</h4> | ||||
|           <div class="metrics"> | ||||
|             ${Object.entries(execution.data.metrics).map(([key, value]) => html` | ||||
|               <div class="metric"> | ||||
|                 <span class="metric-label">${key}</span> | ||||
|                 <span class="metric-value">${typeof value === 'object' ? JSON.stringify(value) : value}</span> | ||||
|               </div> | ||||
|             `)} | ||||
|           </div> | ||||
|         ` : ''} | ||||
|          | ||||
|         ${execution.data.error ? html` | ||||
|           <h4>Error</h4> | ||||
|           <div class="execution-logs"> | ||||
|             <div class="log-entry log-error"> | ||||
|               ${execution.data.error.message} | ||||
|               ${execution.data.error.stack ? html`<pre>${execution.data.error.stack}</pre>` : ''} | ||||
|             </div> | ||||
|           </div> | ||||
|         ` : ''} | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   public render() { | ||||
|     return html` | ||||
|       <cloudly-sectionheading>Tasks</cloudly-sectionheading> | ||||
|        | ||||
|       <div class="filter-bar"> | ||||
|         <button | ||||
|           class="filter-button ${this.filterStatus === 'all' ? 'active' : ''}" | ||||
|           @click=${() => { this.filterStatus = 'all'; this.loadExecutions(); }} | ||||
|         > | ||||
|           All | ||||
|         </button> | ||||
|         <button | ||||
|           class="filter-button ${this.filterStatus === 'running' ? 'active' : ''}" | ||||
|           @click=${() => { this.filterStatus = 'running'; this.loadExecutions(); }} | ||||
|         > | ||||
|           Running | ||||
|         </button> | ||||
|         <button | ||||
|           class="filter-button ${this.filterStatus === 'completed' ? 'active' : ''}" | ||||
|           @click=${() => { this.filterStatus = 'completed'; this.loadExecutions(); }} | ||||
|         > | ||||
|           Completed | ||||
|         </button> | ||||
|         <button | ||||
|           class="filter-button ${this.filterStatus === 'failed' ? 'active' : ''}" | ||||
|           @click=${() => { this.filterStatus = 'failed'; this.loadExecutions(); }} | ||||
|         > | ||||
|           Failed | ||||
|         </button> | ||||
|       </div> | ||||
|        | ||||
|       <div class="task-grid"> | ||||
|         ${this.tasks.map(task => this.renderTaskCard(task))} | ||||
|       </div> | ||||
|        | ||||
|       <cloudly-sectionheading>Execution History</cloudly-sectionheading> | ||||
|        | ||||
|       <dees-table | ||||
|         .heading1=${'Task Executions'} | ||||
|         .heading2=${'History of task runs and their outcomes'} | ||||
|         .data=${this.executions} | ||||
|         .displayFunction=${(itemArg: plugins.interfaces.data.ITaskExecution) => { | ||||
|           return { | ||||
|             Task: itemArg.data.taskName, | ||||
|             Status: html`<span class="status-badge status-${itemArg.data.status}">${itemArg.data.status}</span>`, | ||||
|             'Started At': this.formatDate(itemArg.data.startedAt), | ||||
|             Duration: itemArg.data.duration ? this.formatDuration(itemArg.data.duration) : '-', | ||||
|             'Triggered By': itemArg.data.triggeredBy, | ||||
|             Logs: itemArg.data.logs?.length || 0, | ||||
|           }; | ||||
|         }} | ||||
|         .actionFunction=${async (itemArg) => [ | ||||
|           { | ||||
|             name: 'View Details', | ||||
|             iconName: 'lucide:Eye', | ||||
|             type: ['inRow'], | ||||
|             actionFunc: async () => { | ||||
|               this.selectedExecution = itemArg; | ||||
|             }, | ||||
|           }, | ||||
|         ]} | ||||
|       ></dees-table> | ||||
|        | ||||
|       ${this.selectedExecution ? html` | ||||
|         <cloudly-sectionheading>Execution Details</cloudly-sectionheading> | ||||
|         ${this.renderExecutionDetails(this.selectedExecution)} | ||||
|       ` : ''} | ||||
|     `; | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user