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 | // processes | ||||||
| import { CloudlyCoreflowManager } from './manager.coreflow/coreflowmanager.js'; | import { CloudlyCoreflowManager } from './manager.coreflow/coreflowmanager.js'; | ||||||
| import { ClusterManager } from './manager.cluster/classes.clustermanager.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 { CloudlySecretManager } from './manager.secret/classes.secretmanager.js'; | ||||||
| import { CloudlyNodeManager } from './manager.node/classes.nodemanager.js'; | import { CloudlyNodeManager } from './manager.node/classes.nodemanager.js'; | ||||||
| import { CloudlyBaremetalManager } from './manager.baremetal/classes.baremetalmanager.js'; | import { CloudlyBaremetalManager } from './manager.baremetal/classes.baremetalmanager.js'; | ||||||
| @@ -68,7 +68,7 @@ export class Cloudly { | |||||||
|   public deploymentManager: DeploymentManager; |   public deploymentManager: DeploymentManager; | ||||||
|   public dnsManager: DnsManager; |   public dnsManager: DnsManager; | ||||||
|   public domainManager: DomainManager; |   public domainManager: DomainManager; | ||||||
|   public taskManager: CloudlyTaskmanager; |   public taskManager: CloudlyTaskManager; | ||||||
|   public nodeManager: CloudlyNodeManager; |   public nodeManager: CloudlyNodeManager; | ||||||
|   public baremetalManager: CloudlyBaremetalManager; |   public baremetalManager: CloudlyBaremetalManager; | ||||||
|  |  | ||||||
| @@ -101,7 +101,7 @@ export class Cloudly { | |||||||
|     this.deploymentManager = new DeploymentManager(this); |     this.deploymentManager = new DeploymentManager(this); | ||||||
|     this.dnsManager = new DnsManager(this); |     this.dnsManager = new DnsManager(this); | ||||||
|     this.domainManager = new DomainManager(this); |     this.domainManager = new DomainManager(this); | ||||||
|     this.taskManager = new CloudlyTaskmanager(this); |     this.taskManager = new CloudlyTaskManager(this); | ||||||
|     this.secretManager = new CloudlySecretManager(this); |     this.secretManager = new CloudlySecretManager(this); | ||||||
|     this.nodeManager = new CloudlyNodeManager(this); |     this.nodeManager = new CloudlyNodeManager(this); | ||||||
|     this.baremetalManager = new CloudlyBaremetalManager(this); |     this.baremetalManager = new CloudlyBaremetalManager(this); | ||||||
| @@ -128,6 +128,7 @@ export class Cloudly { | |||||||
|     await this.baremetalManager.start(); |     await this.baremetalManager.start(); | ||||||
|     await this.serviceManager.start(); |     await this.serviceManager.start(); | ||||||
|     await this.deploymentManager.start(); |     await this.deploymentManager.start(); | ||||||
|  |     await this.taskManager.init(); | ||||||
|      |      | ||||||
|     await this.cloudflareConnector.init(); |     await this.cloudflareConnector.init(); | ||||||
|     await this.letsencryptConnector.init(); |     await this.letsencryptConnector.init(); | ||||||
| @@ -150,6 +151,7 @@ export class Cloudly { | |||||||
|     await this.secretManager.stop(); |     await this.secretManager.stop(); | ||||||
|     await this.serviceManager.stop(); |     await this.serviceManager.stop(); | ||||||
|     await this.deploymentManager.stop(); |     await this.deploymentManager.stop(); | ||||||
|  |     await this.taskManager.stop(); | ||||||
|     await this.externalRegistryManager.stop(); |     await this.externalRegistryManager.stop(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import * as plugins from '../plugins.js'; | import * as plugins from '../plugins.js'; | ||||||
|  |  | ||||||
|  | @plugins.smartdata.managed() | ||||||
| export class Deployment extends plugins.smartdata.SmartDataDbDoc< | export class Deployment extends plugins.smartdata.SmartDataDbDoc< | ||||||
|   Deployment, |   Deployment, | ||||||
|   plugins.servezoneInterfaces.data.IDeployment |   plugins.servezoneInterfaces.data.IDeployment | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import * as plugins from '../plugins.js'; | import * as plugins from '../plugins.js'; | ||||||
| import { DnsManager } from './classes.dnsmanager.js'; | import { DnsManager } from './classes.dnsmanager.js'; | ||||||
|  |  | ||||||
|  | @plugins.smartdata.managed() | ||||||
| export class DnsEntry extends plugins.smartdata.SmartDataDbDoc< | export class DnsEntry extends plugins.smartdata.SmartDataDbDoc< | ||||||
|   DnsEntry, |   DnsEntry, | ||||||
|   plugins.servezoneInterfaces.data.IDnsEntry, |   plugins.servezoneInterfaces.data.IDnsEntry, | ||||||
|   | |||||||
| @@ -1,6 +1,7 @@ | |||||||
| import * as plugins from '../plugins.js'; | import * as plugins from '../plugins.js'; | ||||||
| import { DomainManager } from './classes.domainmanager.js'; | import { DomainManager } from './classes.domainmanager.js'; | ||||||
|  |  | ||||||
|  | @plugins.smartdata.managed() | ||||||
| export class Domain extends plugins.smartdata.SmartDataDbDoc< | export class Domain extends plugins.smartdata.SmartDataDbDoc< | ||||||
|   Domain, |   Domain, | ||||||
|   plugins.servezoneInterfaces.data.IDomain, |   plugins.servezoneInterfaces.data.IDomain, | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ import * as paths from '../paths.js'; | |||||||
| import type { Cloudly } from 'ts/classes.cloudly.js'; | import type { Cloudly } from 'ts/classes.cloudly.js'; | ||||||
| import type { ExternalRegistryManager } from './classes.externalregistrymanager.js'; | import type { ExternalRegistryManager } from './classes.externalregistrymanager.js'; | ||||||
|  |  | ||||||
|  | @plugins.smartdata.managed() | ||||||
| export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalRegistry, plugins.servezoneInterfaces.data.IExternalRegistry, ExternalRegistryManager> { | export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc<ExternalRegistry, plugins.servezoneInterfaces.data.IExternalRegistry, ExternalRegistryManager> { | ||||||
|   // STATIC |   // STATIC | ||||||
|   public static async getRegistryById(registryIdArg: string) { |   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']>) { |   public static async createExternalRegistry(registryDataArg: Partial<plugins.servezoneInterfaces.data.IExternalRegistry['data']>) { | ||||||
|     const externalRegistry = new ExternalRegistry(); |     const externalRegistry = new ExternalRegistry(); | ||||||
|     externalRegistry.id = await ExternalRegistry.getNewId(); |     externalRegistry.id = await this.getNewId(); | ||||||
|     externalRegistry.data = { |     externalRegistry.data = { | ||||||
|       type: registryDataArg.type || 'docker', |       type: registryDataArg.type || 'docker', | ||||||
|       name: registryDataArg.name || '', |       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 this is set as default, unset other defaults of the same type | ||||||
|     if (externalRegistry.data.isDefault) { |     if (externalRegistry.data.isDefault) { | ||||||
|       const existingDefaults = await ExternalRegistry.getInstances({ |       const existingDefaults = await this.getInstances({ | ||||||
|         'data.type': externalRegistry.data.type, |         'data.type': externalRegistry.data.type, | ||||||
|         'data.isDefault': true, |         '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 setting as default, unset other defaults of the same type | ||||||
|     if (registryDataArg.isDefault && !externalRegistry.data.isDefault) { |     if (registryDataArg.isDefault && !externalRegistry.data.isDefault) { | ||||||
|       const existingDefaults = await ExternalRegistry.getInstances({ |       const existingDefaults = await this.getInstances({ | ||||||
|         'data.type': externalRegistry.data.type, |         'data.type': externalRegistry.data.type, | ||||||
|         'data.isDefault': true, |         'data.isDefault': true, | ||||||
|       }); |       }); | ||||||
|   | |||||||
| @@ -24,7 +24,7 @@ export class ExternalRegistryManager { | |||||||
|           this.cloudlyRef.authManager.validIdentityGuard, |           this.cloudlyRef.authManager.validIdentityGuard, | ||||||
|         ]); |         ]); | ||||||
|          |          | ||||||
|         const registry = await ExternalRegistry.getRegistryById(dataArg.id); |         const registry = await this.CExternalRegistry.getRegistryById(dataArg.id); | ||||||
|         if (!registry) { |         if (!registry) { | ||||||
|           throw new Error(`Registry with id ${dataArg.id} not found`); |           throw new Error(`Registry with id ${dataArg.id} not found`); | ||||||
|         } |         } | ||||||
| @@ -41,7 +41,7 @@ export class ExternalRegistryManager { | |||||||
|           this.cloudlyRef.authManager.validIdentityGuard, |           this.cloudlyRef.authManager.validIdentityGuard, | ||||||
|         ]); |         ]); | ||||||
|          |          | ||||||
|         const registries = await ExternalRegistry.getRegistries(); |         const registries = await this.CExternalRegistry.getRegistries(); | ||||||
|         return { |         return { | ||||||
|           registries: await Promise.all( |           registries: await Promise.all( | ||||||
|             registries.map((registry) => registry.createSavableObject()) |             registries.map((registry) => registry.createSavableObject()) | ||||||
| @@ -57,7 +57,7 @@ export class ExternalRegistryManager { | |||||||
|           this.cloudlyRef.authManager.validIdentityGuard, |           this.cloudlyRef.authManager.validIdentityGuard, | ||||||
|         ]); |         ]); | ||||||
|          |          | ||||||
|         const registry = await ExternalRegistry.createExternalRegistry(dataArg.registryData); |         const registry = await this.CExternalRegistry.createExternalRegistry(dataArg.registryData); | ||||||
|         return { |         return { | ||||||
|           registry: await registry.createSavableObject(), |           registry: await registry.createSavableObject(), | ||||||
|         }; |         }; | ||||||
| @@ -71,7 +71,7 @@ export class ExternalRegistryManager { | |||||||
|           this.cloudlyRef.authManager.validIdentityGuard, |           this.cloudlyRef.authManager.validIdentityGuard, | ||||||
|         ]); |         ]); | ||||||
|          |          | ||||||
|         const registry = await ExternalRegistry.updateExternalRegistry( |         const registry = await this.CExternalRegistry.updateExternalRegistry( | ||||||
|           dataArg.registryId, |           dataArg.registryId, | ||||||
|           dataArg.registryData |           dataArg.registryData | ||||||
|         ); |         ); | ||||||
| @@ -88,7 +88,7 @@ export class ExternalRegistryManager { | |||||||
|           this.cloudlyRef.authManager.validIdentityGuard, |           this.cloudlyRef.authManager.validIdentityGuard, | ||||||
|         ]); |         ]); | ||||||
|          |          | ||||||
|         const success = await ExternalRegistry.deleteExternalRegistry(dataArg.registryId); |         const success = await this.CExternalRegistry.deleteExternalRegistry(dataArg.registryId); | ||||||
|         return { |         return { | ||||||
|           ok: success, |           ok: success, | ||||||
|         }; |         }; | ||||||
| @@ -102,7 +102,7 @@ export class ExternalRegistryManager { | |||||||
|           this.cloudlyRef.authManager.validIdentityGuard, |           this.cloudlyRef.authManager.validIdentityGuard, | ||||||
|         ]); |         ]); | ||||||
|          |          | ||||||
|         const registry = await ExternalRegistry.getRegistryById(dataArg.registryId); |         const registry = await this.CExternalRegistry.getRegistryById(dataArg.registryId); | ||||||
|         if (!registry) { |         if (!registry) { | ||||||
|           return { |           return { | ||||||
|             success: false, |             success: false, | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import { SecretBundle } from 'ts/manager.secret/classes.secretbundle.js'; | |||||||
| import * as plugins from '../plugins.js'; | import * as plugins from '../plugins.js'; | ||||||
| import { ServiceManager } from './classes.servicemanager.js'; | import { ServiceManager } from './classes.servicemanager.js'; | ||||||
|  |  | ||||||
|  | @plugins.smartdata.managed() | ||||||
| export class Service extends plugins.smartdata.SmartDataDbDoc< | export class Service extends plugins.smartdata.SmartDataDbDoc< | ||||||
|   Service, |   Service, | ||||||
|   plugins.servezoneInterfaces.data.IService, |   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 './settings.js'; | ||||||
| export * from './service.js'; | export * from './service.js'; | ||||||
| export * from './status.js'; | export * from './status.js'; | ||||||
|  | export * from './taskexecution.js'; | ||||||
| export * from './traffic.js'; | export * from './traffic.js'; | ||||||
| export * from './user.js'; | export * from './user.js'; | ||||||
| export * from './version.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 serviceRequests from './service.js'; | ||||||
| import * as settingsRequests from './settings.js'; | import * as settingsRequests from './settings.js'; | ||||||
| import * as statusRequests from './status.js'; | import * as statusRequests from './status.js'; | ||||||
|  | import * as taskRequests from './task.js'; | ||||||
| import * as versionRequests from './version.js'; | import * as versionRequests from './version.js'; | ||||||
|  |  | ||||||
| export { | export { | ||||||
| @@ -47,6 +48,7 @@ export { | |||||||
|   serviceRequests as service, |   serviceRequests as service, | ||||||
|   settingsRequests as settings, |   settingsRequests as settings, | ||||||
|   statusRequests as status, |   statusRequests as status, | ||||||
|  |   taskRequests as task, | ||||||
|   versionRequests as version, |   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 | // cluster | ||||||
| export const addClusterAction = dataState.createAction( | export const addClusterAction = dataState.createAction( | ||||||
|   async ( |   async ( | ||||||
|   | |||||||
| @@ -27,6 +27,7 @@ import { CloudlyViewSecretGroups } from './cloudly-view-secretgroups.js'; | |||||||
| import { CloudlyViewServices } from './cloudly-view-services.js'; | import { CloudlyViewServices } from './cloudly-view-services.js'; | ||||||
| import { CloudlyViewExternalRegistries } from './cloudly-view-externalregistries.js'; | import { CloudlyViewExternalRegistries } from './cloudly-view-externalregistries.js'; | ||||||
| import { CloudlyViewSettings } from './cloudly-view-settings.js'; | import { CloudlyViewSettings } from './cloudly-view-settings.js'; | ||||||
|  | import { CloudlyViewTasks } from './cloudly-view-tasks.js'; | ||||||
|  |  | ||||||
| declare global { | declare global { | ||||||
|   interface HTMLElementTagNameMap { |   interface HTMLElementTagNameMap { | ||||||
| @@ -126,6 +127,11 @@ export class CloudlyDashboard extends DeesElement { | |||||||
|                 iconName: 'lucide:Rocket', |                 iconName: 'lucide:Rocket', | ||||||
|                 element: CloudlyViewDeployments, |                 element: CloudlyViewDeployments, | ||||||
|               }, |               }, | ||||||
|  |               { | ||||||
|  |                 name: 'Tasks', | ||||||
|  |                 iconName: 'lucide:ListChecks', | ||||||
|  |                 element: CloudlyViewTasks, | ||||||
|  |               }, | ||||||
|               { |               { | ||||||
|                 name: 'Domains', |                 name: 'Domains', | ||||||
|                 iconName: 'lucide:Globe2', |                 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