diff --git a/ts/classes.cloudly.ts b/ts/classes.cloudly.ts index 9ab2718..025c693 100644 --- a/ts/classes.cloudly.ts +++ b/ts/classes.cloudly.ts @@ -16,7 +16,7 @@ import { MongodbConnector } from './connector.mongodb/connector.js'; // processes import { CloudlyCoreflowManager } from './manager.coreflow/coreflowmanager.js'; import { ClusterManager } from './manager.cluster/classes.clustermanager.js'; -import { CloudlyTaskmanager } from './manager.task/taskmanager.js'; +import { CloudlyTaskManager } from './manager.task/classes.taskmanager.js'; import { CloudlySecretManager } from './manager.secret/classes.secretmanager.js'; import { CloudlyNodeManager } from './manager.node/classes.nodemanager.js'; import { CloudlyBaremetalManager } from './manager.baremetal/classes.baremetalmanager.js'; @@ -68,7 +68,7 @@ export class Cloudly { public deploymentManager: DeploymentManager; public dnsManager: DnsManager; public domainManager: DomainManager; - public taskManager: CloudlyTaskmanager; + public taskManager: CloudlyTaskManager; public nodeManager: CloudlyNodeManager; public baremetalManager: CloudlyBaremetalManager; @@ -101,7 +101,7 @@ export class Cloudly { this.deploymentManager = new DeploymentManager(this); this.dnsManager = new DnsManager(this); this.domainManager = new DomainManager(this); - this.taskManager = new CloudlyTaskmanager(this); + this.taskManager = new CloudlyTaskManager(this); this.secretManager = new CloudlySecretManager(this); this.nodeManager = new CloudlyNodeManager(this); this.baremetalManager = new CloudlyBaremetalManager(this); @@ -128,6 +128,7 @@ export class Cloudly { await this.baremetalManager.start(); await this.serviceManager.start(); await this.deploymentManager.start(); + await this.taskManager.init(); await this.cloudflareConnector.init(); await this.letsencryptConnector.init(); @@ -150,6 +151,7 @@ export class Cloudly { await this.secretManager.stop(); await this.serviceManager.stop(); await this.deploymentManager.stop(); + await this.taskManager.stop(); await this.externalRegistryManager.stop(); } } diff --git a/ts/manager.deployment/classes.deployment.ts b/ts/manager.deployment/classes.deployment.ts index 0ac7251..a6785b4 100644 --- a/ts/manager.deployment/classes.deployment.ts +++ b/ts/manager.deployment/classes.deployment.ts @@ -1,5 +1,6 @@ import * as plugins from '../plugins.js'; +@plugins.smartdata.managed() export class Deployment extends plugins.smartdata.SmartDataDbDoc< Deployment, plugins.servezoneInterfaces.data.IDeployment diff --git a/ts/manager.dns/classes.dnsentry.ts b/ts/manager.dns/classes.dnsentry.ts index 1cee4b5..fc48614 100644 --- a/ts/manager.dns/classes.dnsentry.ts +++ b/ts/manager.dns/classes.dnsentry.ts @@ -1,6 +1,7 @@ import * as plugins from '../plugins.js'; import { DnsManager } from './classes.dnsmanager.js'; +@plugins.smartdata.managed() export class DnsEntry extends plugins.smartdata.SmartDataDbDoc< DnsEntry, plugins.servezoneInterfaces.data.IDnsEntry, diff --git a/ts/manager.domain/classes.domain.ts b/ts/manager.domain/classes.domain.ts index 3cfd336..9a0f29f 100644 --- a/ts/manager.domain/classes.domain.ts +++ b/ts/manager.domain/classes.domain.ts @@ -1,6 +1,7 @@ import * as plugins from '../plugins.js'; import { DomainManager } from './classes.domainmanager.js'; +@plugins.smartdata.managed() export class Domain extends plugins.smartdata.SmartDataDbDoc< Domain, plugins.servezoneInterfaces.data.IDomain, diff --git a/ts/manager.externalregistry/classes.externalregistry.ts b/ts/manager.externalregistry/classes.externalregistry.ts index f5f95a7..12f4a7e 100644 --- a/ts/manager.externalregistry/classes.externalregistry.ts +++ b/ts/manager.externalregistry/classes.externalregistry.ts @@ -3,6 +3,7 @@ import * as paths from '../paths.js'; import type { Cloudly } from 'ts/classes.cloudly.js'; import type { ExternalRegistryManager } from './classes.externalregistrymanager.js'; +@plugins.smartdata.managed() export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc { // STATIC public static async getRegistryById(registryIdArg: string) { @@ -27,7 +28,7 @@ export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc) { const externalRegistry = new ExternalRegistry(); - externalRegistry.id = await ExternalRegistry.getNewId(); + externalRegistry.id = await this.getNewId(); externalRegistry.data = { type: registryDataArg.type || 'docker', name: registryDataArg.name || '', @@ -48,7 +49,7 @@ export class ExternalRegistry extends plugins.smartdata.SmartDataDbDoc registry.createSavableObject()) @@ -57,7 +57,7 @@ export class ExternalRegistryManager { this.cloudlyRef.authManager.validIdentityGuard, ]); - const registry = await ExternalRegistry.createExternalRegistry(dataArg.registryData); + const registry = await this.CExternalRegistry.createExternalRegistry(dataArg.registryData); return { registry: await registry.createSavableObject(), }; @@ -71,7 +71,7 @@ export class ExternalRegistryManager { this.cloudlyRef.authManager.validIdentityGuard, ]); - const registry = await ExternalRegistry.updateExternalRegistry( + const registry = await this.CExternalRegistry.updateExternalRegistry( dataArg.registryId, dataArg.registryData ); @@ -88,7 +88,7 @@ export class ExternalRegistryManager { this.cloudlyRef.authManager.validIdentityGuard, ]); - const success = await ExternalRegistry.deleteExternalRegistry(dataArg.registryId); + const success = await this.CExternalRegistry.deleteExternalRegistry(dataArg.registryId); return { ok: success, }; @@ -102,7 +102,7 @@ export class ExternalRegistryManager { this.cloudlyRef.authManager.validIdentityGuard, ]); - const registry = await ExternalRegistry.getRegistryById(dataArg.registryId); + const registry = await this.CExternalRegistry.getRegistryById(dataArg.registryId); if (!registry) { return { success: false, diff --git a/ts/manager.service/classes.service.ts b/ts/manager.service/classes.service.ts index 4207e8d..85a7945 100644 --- a/ts/manager.service/classes.service.ts +++ b/ts/manager.service/classes.service.ts @@ -2,6 +2,7 @@ import { SecretBundle } from 'ts/manager.secret/classes.secretbundle.js'; import * as plugins from '../plugins.js'; import { ServiceManager } from './classes.servicemanager.js'; +@plugins.smartdata.managed() export class Service extends plugins.smartdata.SmartDataDbDoc< Service, plugins.servezoneInterfaces.data.IService, diff --git a/ts/manager.task/classes.taskexecution.ts b/ts/manager.task/classes.taskexecution.ts new file mode 100644 index 0000000..7c2f672 --- /dev/null +++ b/ts/manager.task/classes.taskexecution.ts @@ -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; + } +} \ No newline at end of file diff --git a/ts/manager.task/classes.taskmanager.ts b/ts/manager.task/classes.taskmanager.ts new file mode 100644 index 0000000..fb410c0 --- /dev/null +++ b/ts/manager.task/classes.taskmanager.ts @@ -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(); + private taskInfo = new Map(); + private currentExecutions = new Map(); + + // 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 + ) { + 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 { + 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 { + 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( + '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( + '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( + '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( + '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( + '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'); + } +} \ No newline at end of file diff --git a/ts/manager.task/predefinedtasks.ts b/ts/manager.task/predefinedtasks.ts new file mode 100644 index 0000000..8ad1b9a --- /dev/null +++ b/ts/manager.task/predefinedtasks.ts @@ -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'); +} \ No newline at end of file diff --git a/ts/manager.task/taskmanager.ts b/ts/manager.task/taskmanager.ts deleted file mode 100644 index c2b10e4..0000000 --- a/ts/manager.task/taskmanager.ts +++ /dev/null @@ -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 () => {}, - }); -} diff --git a/ts_interfaces/data/index.ts b/ts_interfaces/data/index.ts index 40ba863..0875717 100644 --- a/ts_interfaces/data/index.ts +++ b/ts_interfaces/data/index.ts @@ -15,6 +15,7 @@ export * from './clusternode.js'; export * from './settings.js'; export * from './service.js'; export * from './status.js'; +export * from './taskexecution.js'; export * from './traffic.js'; export * from './user.js'; export * from './version.js'; diff --git a/ts_interfaces/data/taskexecution.ts b/ts_interfaces/data/taskexecution.ts new file mode 100644 index 0000000..4206aba --- /dev/null +++ b/ts_interfaces/data/taskexecution.ts @@ -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; + }; + }; +} \ No newline at end of file diff --git a/ts_interfaces/requests/index.ts b/ts_interfaces/requests/index.ts index 93b5a93..f3799a3 100644 --- a/ts_interfaces/requests/index.ts +++ b/ts_interfaces/requests/index.ts @@ -22,6 +22,7 @@ import * as serverRequests from './server.js'; import * as serviceRequests from './service.js'; import * as settingsRequests from './settings.js'; import * as statusRequests from './status.js'; +import * as taskRequests from './task.js'; import * as versionRequests from './version.js'; export { @@ -47,6 +48,7 @@ export { serviceRequests as service, settingsRequests as settings, statusRequests as status, + taskRequests as task, versionRequests as version, }; diff --git a/ts_interfaces/requests/task.ts b/ts_interfaces/requests/task.ts new file mode 100644 index 0000000..492d46e --- /dev/null +++ b/ts_interfaces/requests/task.ts @@ -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; + }; +} \ No newline at end of file diff --git a/ts_web/appstate.ts b/ts_web/appstate.ts index 3d216b7..917294c 100644 --- a/ts_web/appstate.ts +++ b/ts_web/appstate.ts @@ -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( + '/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( + '/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( + '/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( + '/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( + '/typedrequest', + 'cancelTask' + ); + const response = await trCancelTask.fire({ + identity: loginStatePart.getState().identity, + executionId: payloadArg.executionId, + }); + return currentState; + } + ), +}; + // cluster export const addClusterAction = dataState.createAction( async ( diff --git a/ts_web/elements/cloudly-dashboard.ts b/ts_web/elements/cloudly-dashboard.ts index 89af155..513c0f2 100644 --- a/ts_web/elements/cloudly-dashboard.ts +++ b/ts_web/elements/cloudly-dashboard.ts @@ -27,6 +27,7 @@ import { CloudlyViewSecretGroups } from './cloudly-view-secretgroups.js'; import { CloudlyViewServices } from './cloudly-view-services.js'; import { CloudlyViewExternalRegistries } from './cloudly-view-externalregistries.js'; import { CloudlyViewSettings } from './cloudly-view-settings.js'; +import { CloudlyViewTasks } from './cloudly-view-tasks.js'; declare global { interface HTMLElementTagNameMap { @@ -126,6 +127,11 @@ export class CloudlyDashboard extends DeesElement { iconName: 'lucide:Rocket', element: CloudlyViewDeployments, }, + { + name: 'Tasks', + iconName: 'lucide:ListChecks', + element: CloudlyViewTasks, + }, { name: 'Domains', iconName: 'lucide:Globe2', diff --git a/ts_web/elements/cloudly-view-tasks.ts b/ts_web/elements/cloudly-view-tasks.ts new file mode 100644 index 0000000..f0c5fa5 --- /dev/null +++ b/ts_web/elements/cloudly-view-tasks.ts @@ -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` +
+
+
+ + ${task.name} +
+ +
+ +
${task.description}
+ +
+ ${task.category} + ${!task.enabled ? html`Disabled` : ''} +
+ + ${task.schedule ? html` +
+ + Schedule: ${task.schedule} +
+ ` : ''} + + ${task.lastRun ? html` +
+ Last run: ${this.formatDate(task.lastRun)} +
+ ` : ''} + + ${lastExecution ? html` +
+
+ Status + + + ${lastExecution.data.status} + + +
+ ${lastExecution.data.duration ? html` +
+ Duration + ${this.formatDuration(lastExecution.data.duration)} +
+ ` : ''} +
+ ` : ''} +
+ `; + } + + private renderExecutionDetails(execution: plugins.interfaces.data.ITaskExecution) { + return html` +
+

Execution Details: ${execution.data.taskName}

+ +
+
+ Started + ${this.formatDate(execution.data.startedAt)} +
+ ${execution.data.completedAt ? html` +
+ Completed + ${this.formatDate(execution.data.completedAt)} +
+ ` : ''} + ${execution.data.duration ? html` +
+ Duration + ${this.formatDuration(execution.data.duration)} +
+ ` : ''} +
+ Triggered By + ${execution.data.triggeredBy} +
+
+ + ${execution.data.logs && execution.data.logs.length > 0 ? html` +

Logs

+
+ ${execution.data.logs.map(log => html` +
+ ${this.formatDate(log.timestamp)} - + ${log.message} +
+ `)} +
+ ` : ''} + + ${execution.data.metrics ? html` +

Metrics

+
+ ${Object.entries(execution.data.metrics).map(([key, value]) => html` +
+ ${key} + ${typeof value === 'object' ? JSON.stringify(value) : value} +
+ `)} +
+ ` : ''} + + ${execution.data.error ? html` +

Error

+
+
+ ${execution.data.error.message} + ${execution.data.error.stack ? html`
${execution.data.error.stack}
` : ''} +
+
+ ` : ''} +
+ `; + } + + public render() { + return html` + Tasks + +
+ + + + +
+ +
+ ${this.tasks.map(task => this.renderTaskCard(task))} +
+ + Execution History + + { + return { + Task: itemArg.data.taskName, + Status: html`${itemArg.data.status}`, + '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; + }, + }, + ]} + > + + ${this.selectedExecution ? html` + Execution Details + ${this.renderExecutionDetails(this.selectedExecution)} + ` : ''} + `; + } +} \ No newline at end of file