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 selectedExecution: plugins.interfaces.data.ITaskExecution | null = null; @state() private loading = false; @state() private filterStatus: string = 'all'; @state() private searchQuery: string = ''; @state() private categoryFilter: string = 'all'; @state() private autoRefresh: boolean = true; private _refreshHandle: any = null; constructor() { super(); const subscription = appstate.dataState .select((stateArg) => stateArg) .subscribe((dataArg) => { this.data = dataArg; }); this.rxSubscriptions.push(subscription); // Load initial data (non-blocking) this.loadInitialData(); // Start periodic refresh (lightweight; executions only by default) this.startAutoRefresh(); } private async loadInitialData() { try { await appstate.dataState.dispatchAction(appstate.taskActions.getTasks, {}); await appstate.dataState.dispatchAction(appstate.taskActions.getTaskExecutions, {}); } catch (error) { console.error('Failed to load initial task data:', error); } } private startAutoRefresh() { this.stopAutoRefresh(); if (!this.autoRefresh) return; this._refreshHandle = setInterval(async () => { try { await this.loadExecutionsWithFilter(); } catch (err) { // ignore transient errors during refresh } }, 5000); } private stopAutoRefresh() { if (this._refreshHandle) { clearInterval(this._refreshHandle); this._refreshHandle = null; } } public async disconnectedCallback(): Promise { await (super.disconnectedCallback?.()); this.stopAutoRefresh(); } public static styles = [ cssManager.defaultStyles, shared.viewHostCss, css` .toolbar { display: flex; gap: 12px; align-items: center; margin: 4px 0 16px 0; flex-wrap: wrap; } .toolbar .spacer { flex: 1 1 auto; } .search-input { background: #111; color: #ddd; border: 1px solid #333; border-radius: 6px; padding: 8px 10px; min-width: 220px; } .chipbar { display: flex; gap: 8px; flex-wrap: wrap; } .chip { padding: 6px 10px; background: #2a2a2a; color: #bbb; border: 1px solid #444; border-radius: 16px; cursor: pointer; transition: all 0.2s; user-select: none; } .chip.active { background: #2196f3; border-color: #2196f3; color: white; } .task-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); gap: 16px; margin-bottom: 32px; } .task-card { background: #131313; border: 1px solid #2a2a2a; border-radius: 10px; padding: 16px; transition: border-color 0.2s, background 0.2s; } .task-card:hover { border-color: #3a3a3a; } .card-header { display: flex; justify-content: space-between; align-items: center; gap: 12px; margin-bottom: 12px; } .header-left { display: flex; align-items: center; gap: 12px; min-width: 0; } .header-right { display: flex; align-items: center; gap: 8px; } .task-icon { color: #cfcfcf; } .task-name { font-size: 1.05em; font-weight: 650; color: #fff; letter-spacing: 0.1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } .task-subtitle { color: #8c8c8c; font-size: 0.9em; } .task-description { color: #b5b5b5; font-size: 0.95em; margin-bottom: 12px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .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: 6px; 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: 6px; 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; } .card-actions, .task-header .right { display: flex; gap: 8px; align-items: center; } .metrics-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; margin-top: 8px; } .metric-item { background: #0f0f0f; border: 1px solid #2c2c2c; border-radius: 8px; padding: 10px 12px; } .metric-item .label { color: #8d8d8d; font-size: 0.8em; } .metric-item .value { color: #eaeaea; font-weight: 600; margin-top: 4px; } .lastline { display: flex; align-items: center; gap: 8px; color: #a0a0a0; font-size: 0.9em; margin-top: 10px; } .dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; } .dot.info { background: #2196f3; } .dot.success { background: #4caf50; } .dot.warning { background: #ff9800; } .dot.error { background: #f44336; } .card-footer { display: flex; gap: 12px; margin-top: 12px; } .link-button { background: transparent; border: none; color: #8ab4ff; cursor: pointer; padding: 0; font-size: 0.95em; } .link-button:hover { text-decoration: underline; } .secondary-button { padding: 6px 12px; background: #2b2b2b; color: #ccc; border: 1px solid #444; border-radius: 6px; cursor: pointer; font-size: 0.9em; transition: background 0.2s, border-color 0.2s; } .secondary-button:hover { background: #363636; border-color: #555; } `, ]; private async triggerTask(taskName: string) { try { const modal = await plugins.deesCatalog.DeesModal.createAndShow({ heading: `Run Task: ${taskName}`, content: html`

Do you want to trigger this task now?

`, menuOptions: [ { name: 'Run now', action: async (modalArg: any) => { await appstate.dataState.dispatchAction( appstate.taskActions.triggerTask, { taskName } ); plugins.deesCatalog.DeesToast.createAndShow({ message: `Task ${taskName} triggered`, type: 'success', }); await modalArg.destroy(); // Refresh executions to reflect the new run quickly await this.loadExecutionsWithFilter(); } }, { name: 'Cancel', action: async (modalArg: any) => modalArg.destroy() } ] }); } catch (error) { console.error('Failed to trigger task:', error); plugins.deesCatalog.DeesToast.createAndShow({ message: `Failed to trigger: ${error.message}`, type: 'error', }); } } private async cancelTaskFor(taskName: string) { try { const executions = (this.data.taskExecutions || []) .filter(e => e.data.taskName === taskName && e.data.status === 'running') .sort((a, b) => (b.data.startedAt || 0) - (a.data.startedAt || 0)); const running = executions[0]; if (!running) return; await appstate.dataState.dispatchAction( appstate.taskActions.cancelTask, { executionId: running.id } ); plugins.deesCatalog.DeesToast.createAndShow({ message: `Cancelled ${taskName}`, type: 'success', }); await this.loadExecutionsWithFilter(); } catch (err) { console.error('Failed to cancel task:', err); plugins.deesCatalog.DeesToast.createAndShow({ message: `Cancel failed: ${err.message}`, type: 'error', }); } } private async loadExecutionsWithFilter() { try { const filter: any = {}; if (this.filterStatus !== 'all') { filter.status = this.filterStatus; } await appstate.dataState.dispatchAction( appstate.taskActions.getTaskExecutions, { filter } ); } catch (error) { console.error('Failed to load executions:', 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 formatRelativeTime(ts?: number): string { if (!ts) return '-'; const diff = Date.now() - ts; const abs = Math.abs(diff); if (abs < 60_000) return `${Math.round(abs / 1000)}s ago`; if (abs < 3_600_000) return `${Math.round(abs / 60_000)}m ago`; if (abs < 86_400_000) return `${Math.round(abs / 3_600_000)}h ago`; return `${Math.round(abs / 86_400_000)}d ago`; } 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 getCategoryHue(category: string): number { switch (category) { case 'maintenance': return 28; // orange case 'deployment': return 208; // blue case 'backup': return 122; // green case 'monitoring': return 280; // purple case 'cleanup': return 20; // brownish case 'system': return 200; // steel case 'security': return 0; // red default: return 210; // default blue } } private getStatusColor(status?: string): string { switch (status) { case 'running': return '#2196f3'; case 'completed': return '#4caf50'; case 'failed': return '#f44336'; case 'cancelled': return '#ff9800'; default: return '#3a3a3a'; } } private formatCronFriendly(cron?: string): string { if (!cron) return ''; const parts = cron.trim().split(/\s+/); if (parts.length !== 5) return cron; // fallback const [min, hour, dom, mon, dow] = parts; if (min === '*/1' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return 'every minute'; if (min.startsWith('*/') && hour === '*' && dom === '*' && mon === '*' && dow === '*') return `every ${min.replace('*/','')} min`; if (min === '0' && hour.startsWith('*/') && dom === '*' && mon === '*' && dow === '*') return `every ${hour.replace('*/','')} hours`; if (min === '0' && hour === '*' && dom === '*' && mon === '*' && dow === '*') return 'hourly'; if (min === '0' && hour === '0' && dom === '*' && mon === '*' && dow === '*') return 'daily'; if (min === '0' && hour === '0' && dom === '1' && mon === '*' && dow === '*') return 'monthly'; return cron; } private renderTaskCard(task: any) { const executions = this.data.taskExecutions || []; const lastExecution = 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'; const executionsForTask = executions.filter(e => e.data.taskName === task.name); const now = Date.now(); const last24hCount = executionsForTask.filter(e => (e.data.startedAt || 0) > now - 86_400_000).length; const completed = executionsForTask.filter(e => e.data.status === 'completed'); const successRate = executionsForTask.length ? Math.round((completed.length * 100) / executionsForTask.length) : 0; const avgDuration = completed.length ? Math.round(completed.reduce((acc, e) => acc + (e.data.duration || 0), 0) / completed.length) : undefined; const lastLog = lastExecution?.data.logs && lastExecution.data.logs.length > 0 ? lastExecution.data.logs[lastExecution.data.logs.length - 1] : null; const status = lastExecution?.data.status as ('running'|'completed'|'failed'|'cancelled'|undefined); const hue = this.getCategoryHue(task.category); const subtitle = [ task.category, task.schedule ? `⏱ ${this.formatCronFriendly(task.schedule)}` : null, isRunning ? (lastExecution?.data.startedAt ? `Started ${this.formatRelativeTime(lastExecution.data.startedAt)}` : 'Running') : (task.lastRun ? `Last ${this.formatRelativeTime(task.lastRun)}` : 'Never run') ].filter(Boolean).join(' • '); return html`
${task.name}
${subtitle}
${lastExecution ? html`${lastExecution.data.status}` : html`idle`} ${isRunning ? html` this.cancelTaskFor(task.name)}> ` : html` this.triggerTask(task.name)}> `}
${task.description}
${lastExecution ? html`
Last Status
${lastExecution.data.status}
Avg Duration
${avgDuration ? this.formatDuration(avgDuration) : '-'}
24h Runs · Success
${last24hCount} · ${successRate}%
${lastLog ? html` ${lastLog.message}` : 'No recent logs'}
` : html`
Last Status
Avg Duration
24h Runs · Success
0 · 0%
`}
`; } private async openExecutionDetails(execution: plugins.interfaces.data.ITaskExecution) { this.selectedExecution = execution; // Scroll into view of the details section requestAnimationFrame(() => { this.shadowRoot?.querySelector('cloudly-sectionheading + .execution-details')?.scrollIntoView({ behavior: 'smooth', block: 'start' }); }); } private async openLogsModal(execution: plugins.interfaces.data.ITaskExecution) { await plugins.deesCatalog.DeesModal.createAndShow({ heading: `Logs: ${execution.data.taskName}`, content: html`
${(execution.data.logs || []).map(log => html`
${this.formatDate(log.timestamp)} - ${log.message}
`)}
`, menuOptions: [ { name: 'Copy All', action: async (modalArg: any) => { try { await navigator.clipboard.writeText((execution.data.logs || []) .map(l => `${new Date(l.timestamp).toISOString()} [${l.severity}] ${l.message}`).join('\n')); plugins.deesCatalog.DeesToast.createAndShow({ message: 'Logs copied', type: 'success' }); } catch (e) { plugins.deesCatalog.DeesToast.createAndShow({ message: 'Copy failed', type: 'error' }); } } }, { name: 'Close', action: async (modalArg: any) => modalArg.destroy() } ] }); } 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() { const tasks = (this.data.tasks || []) as any[]; const categories = Array.from(new Set(tasks.map(t => t.category))).sort(); const filteredTasks = tasks .filter(t => this.categoryFilter === 'all' || t.category === this.categoryFilter) .filter(t => !this.searchQuery || t.name.toLowerCase().includes(this.searchQuery.toLowerCase()) || (t.description || '').toLowerCase().includes(this.searchQuery.toLowerCase())); return html` Tasks
{ this.categoryFilter = 'all'; }}> All
${categories.map(cat => html`
{ this.categoryFilter = cat; }}> ${cat}
`)}
{ this.searchQuery = e.target.value; }} />
${filteredTasks.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: plugins.interfaces.data.ITaskExecution) => { const actions: any[] = [ { name: 'View Details', iconName: 'lucide:Eye', type: ['inRow'], actionFunc: async () => { this.selectedExecution = itemArg; }, } ]; if (itemArg.data.status === 'running') { actions.push({ name: 'Cancel', iconName: 'lucide:SquareX', type: ['inRow'], actionFunc: async () => { await appstate.dataState.dispatchAction(appstate.taskActions.cancelTask, { executionId: itemArg.id }); await this.loadExecutionsWithFilter(); }, }); } return actions; }} > ${this.selectedExecution ? html` Execution Details ${this.renderExecutionDetails(this.selectedExecution)} ` : ''} `; } }