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