Files
dashcaddy/status/js/bundled-workflows.js
T

455 lines
14 KiB
JavaScript

// ========== BUNDLED WORKFLOWS ==========
(function() {
const WORKFLOW_DEFINITIONS = {
'auto-restart-on-crash': {
name: 'Auto-Restart on Crash',
icon: '🔄',
description: 'Automatically restart a container when it goes down',
trigger: 'container-down',
actions: 'restart + notify'
},
'backup-before-update': {
name: 'Backup Before Update',
icon: '💾',
description: 'Create a backup before any app update',
trigger: 'pre-update',
actions: 'backup + notify'
},
'health-check-on-interval': {
name: 'Periodic Health Check',
icon: '🏥',
description: 'Run health checks every 15 minutes and alert if degraded',
trigger: 'scheduled (15m)',
actions: 'health-check + alert'
},
'disk-space-alert': {
name: 'Disk Space Alert',
icon: '⚠️',
description: 'Alert when disk usage exceeds 80%',
trigger: 'resource-alert',
actions: 'notify'
},
'weekly-container-report': {
name: 'Weekly Container Report',
icon: '📊',
description: 'Send a weekly summary of container status and resource usage',
trigger: 'scheduled (weekly)',
actions: 'collect metrics + report'
}
};
let isPremium = false;
// === CHECK PREMIUM ===
async function checkPremium() {
try {
const resp = await fetch('/api/v1/license/feature/workflows');
const data = await resp.json();
isPremium = data.available;
} catch {
isPremium = false;
}
return isPremium;
}
// === RENDER WORKFLOW CARD ===
function renderWorkflowCard(workflow) {
const def = WORKFLOW_DEFINITIONS[workflow.id] || {
name: workflow.name || workflow.id,
icon: '⚡',
description: workflow.description || '',
trigger: workflow.trigger || 'unknown',
actions: workflow.actions ? workflow.actions.map(a => a.type).join(' + ') : ''
};
const locked = !isPremium;
const cardClass = locked ? 'workflow-card locked' : 'workflow-card';
return `<div class="${cardClass}" data-workflow="${workflow.id}">
<div class="workflow-header">
<span class="workflow-name">${def.icon} ${escapeHtml(def.name)}</span>
<label class="toggle-switch">
<input type="checkbox" class="workflow-toggle" data-workflow="${workflow.id}" ${workflow.enabled ? 'checked' : ''} ${locked ? 'disabled' : ''} />
<span class="slider"></span>
</label>
</div>
<p class="workflow-desc">${escapeHtml(def.description)}</p>
<div class="workflow-meta">
<span class="workflow-trigger">⚡ Trigger: ${escapeHtml(def.trigger)}</span>
<span class="workflow-actions">▶ Actions: ${escapeHtml(def.actions)}</span>
</div>
${locked ? `<div class="workflow-locked-overlay">
<span class="lock-icon">🔒</span>
<span class="lock-text">Upgrade to Enable</span>
</div>` : ''}
<button class="btn-run-now" data-workflow="${workflow.id}" ${locked ? 'disabled' : ''}>
▶ Run Now
</button>
</div>`;
}
// === LOAD WORKFLOWS TAB ===
async function loadWorkflowsTab() {
const container = document.getElementById('workflow-list-container');
if (!container) return;
container.innerHTML = '<div class="panel-empty"><span class="brand-spinner"></span> Loading workflows...</div>';
try {
const resp = await fetch('/api/v1/workflows');
const data = await resp.json();
if (data.success && data.workflows) {
if (data.workflows.length === 0) {
container.innerHTML = '<div class="panel-empty"><span class="empty-icon">⚡</span>No workflows configured</div>';
return;
}
let html = '';
for (const workflow of data.workflows) {
html += renderWorkflowCard(workflow);
}
// Add non-premium banner
if (!isPremium) {
html += `<div class="accent-info-box" style="margin-top: 16px; text-align: center;">
<span>🔒 Upgrade to unlock automated workflows</span>
<button onclick="showNotification('Upgrade to Premium to enable workflows!', 'info'); scrollToSection('license');" style="margin-left: 12px; padding: 6px 16px; background: linear-gradient(135deg, #f39c12, #e67e22); border: none; color: white; border-radius: 6px; cursor: pointer; font-weight: 600;">Upgrade to Premium</button>
</div>`;
}
container.innerHTML = `<div class="workflow-list">${html}</div>`;
wireWorkflowEvents();
} else {
container.innerHTML = '<div class="panel-empty"><span class="empty-icon">⚠️</span>Failed to load workflows</div>';
}
} catch (error) {
container.innerHTML = '<div class="panel-empty"><span class="empty-icon">⚠️</span>Error loading workflows: ' + escapeHtml(error.message) + '</div>';
}
}
// === LOAD HISTORY TAB ===
async function loadHistoryTab() {
const container = document.getElementById('workflow-history-body');
if (!container) return;
container.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 20px;"><span class="brand-spinner"></span> Loading history...</td></tr>';
try {
const resp = await fetch('/api/v1/workflows/history?limit=100');
const data = await resp.json();
if (data.success && data.history && data.history.length > 0) {
let html = '';
for (const entry of data.history) {
const time = new Date(entry.timestamp).toLocaleString();
const duration = entry.duration ? entry.duration + 'ms' : '-';
const resultClass = entry.success ? 'result-success' : 'result-failure';
const resultText = entry.success ? '✓ Success' : '✗ Failed';
html += `<tr>
<td>${escapeHtml(time)}</td>
<td>${escapeHtml(entry.workflowName || entry.workflowId)}</td>
<td>${escapeHtml(entry.trigger || 'manual')}</td>
<td class="${resultClass}">${resultText}</td>
<td>${escapeHtml(duration)}</td>
</tr>`;
}
container.innerHTML = html;
} else {
container.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 20px; color: var(--muted);">No workflow history yet</td></tr>';
}
} catch (error) {
container.innerHTML = '<tr><td colspan="5" style="text-align: center; padding: 20px; color: var(--bad-fg);">Error loading history</td></tr>';
}
}
// === WIRE WORKFLOW EVENTS ===
function wireWorkflowEvents() {
// Toggle switches
document.querySelectorAll('.workflow-toggle').forEach(toggle => {
toggle.addEventListener('change', async function() {
const workflowId = this.dataset.workflow;
const enabled = this.checked;
const endpoint = enabled ? 'enable' : 'disable';
try {
const resp = await fetch(`/api/v1/workflows/${workflowId}/${endpoint}`, { method: 'POST' });
const data = await resp.json();
if (!data.success) {
showNotification(`Failed to ${endpoint} workflow`, 'error', 3000);
this.checked = !enabled; // revert
}
} catch (error) {
showNotification(`Error: ${error.message}`, 'error', 3000);
this.checked = !enabled; // revert
}
});
});
// Run Now buttons
document.querySelectorAll('.btn-run-now').forEach(btn => {
btn.addEventListener('click', async function() {
const workflowId = this.dataset.workflow;
const originalText = this.innerHTML;
this.innerHTML = '<span class="brand-spinner"></span>';
this.disabled = true;
try {
const resp = await fetch(`/api/v1/workflows/${workflowId}/run`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ trigger: 'manual' })
});
const data = await resp.json();
if (data.success) {
showNotification(`Workflow "${WORKFLOW_DEFINITIONS[workflowId]?.name || workflowId}" executed successfully`, 'success', 3000);
} else {
showNotification(`Workflow failed: ${data.error || 'Unknown error'}`, 'error', 4000);
}
} catch (error) {
showNotification(`Error: ${error.message}`, 'error', 3000);
} finally {
this.innerHTML = originalText;
this.disabled = false;
}
});
});
}
// === TAB SWITCHING ===
function setupTabSwitching() {
document.querySelectorAll('.workflows-tab-btn').forEach(btn => {
btn.addEventListener('click', function() {
const panelId = this.dataset.panel;
// Update tab buttons
document.querySelectorAll('.workflows-tab-btn').forEach(b => b.classList.remove('active'));
this.classList.add('active');
// Update panels
document.querySelectorAll('.workflow-panel').forEach(p => p.classList.remove('active'));
document.getElementById(panelId)?.classList.add('active');
// Load data for the active panel
if (panelId === 'workflows-list-panel') {
loadWorkflowsTab();
} else if (panelId === 'workflows-history-panel') {
loadHistoryTab();
}
});
});
}
// === INJECT MODAL HTML ===
injectModal('bundled-workflows-modal', `<div id="bundled-workflows-modal" class="weather-modal">
<div class="weather-modal-content" style="min-width: 600px; max-width: 750px;">
<h3>⚡ Bundled Workflows</h3>
<p class="modal-subtitle">
Automated workflows to keep your system running smoothly
</p>
<!-- Tab bar -->
<div class="panel-tabs">
<button class="workflows-tab-btn panel-tab active" data-panel="workflows-list-panel">Workflows</button>
<button class="workflows-tab-btn panel-tab" data-panel="workflows-history-panel">History</button>
</div>
<!-- Tab: Workflows -->
<div id="workflows-list-panel" class="workflow-panel panel-section active">
<div id="workflow-list-container">
<div class="panel-empty">
<span class="brand-spinner"></span> Loading workflows...
</div>
</div>
</div>
<!-- Tab: History -->
<div id="workflows-history-panel" class="workflow-panel panel-section">
<div class="workflow-history">
<table>
<thead>
<tr>
<th>Time</th>
<th>Workflow</th>
<th>Trigger</th>
<th>Result</th>
<th>Duration</th>
</tr>
</thead>
<tbody id="workflow-history-body">
<!-- populated from API -->
</tbody>
</table>
</div>
</div>
<!-- Close Button -->
<div class="weather-modal-buttons modal-footer-bar">
<button id="workflows-cancel">Close</button>
</div>
</div>
</div>`);
const modal = document.getElementById('bundled-workflows-modal');
const openBtn = document.getElementById('bundled-workflows-btn');
const cancelBtn = document.getElementById('workflows-cancel');
// === STYLES ===
const style = document.createElement('style');
style.textContent = `
.workflow-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.workflow-card {
position: relative;
padding: 16px;
background: var(--card-base);
border: 1px solid var(--border);
border-radius: 10px;
transition: all 0.2s ease;
}
.workflow-card:hover {
border-color: var(--accent);
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.workflow-card.locked {
opacity: 0.7;
}
.workflow-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.workflow-name {
font-weight: 600;
font-size: 0.95rem;
}
.workflow-desc {
font-size: 0.82rem;
color: var(--muted);
margin: 0 0 10px 0;
line-height: 1.4;
}
.workflow-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
font-size: 0.75rem;
color: var(--muted);
margin-bottom: 12px;
}
.workflow-meta span {
padding: 2px 8px;
background: var(--base);
border-radius: 4px;
}
.workflow-locked-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
border-radius: 10px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 8px;
}
.lock-icon {
font-size: 1.5rem;
}
.lock-text {
font-size: 0.8rem;
font-weight: 600;
color: #f39c12;
}
.workflow-history {
max-height: 400px;
overflow-y: auto;
}
.workflow-history table {
width: 100%;
border-collapse: collapse;
font-size: 0.82rem;
}
.workflow-history th {
text-align: left;
padding: 8px 10px;
background: var(--base);
border-bottom: 1px solid var(--border);
font-weight: 600;
position: sticky;
top: 0;
}
.workflow-history td {
padding: 8px 10px;
border-bottom: 1px solid var(--border);
}
.result-success {
color: var(--ok-fg);
font-weight: 500;
}
.result-failure {
color: var(--bad-fg);
font-weight: 500;
}
.workflow-panel {
display: none;
}
.workflow-panel.active {
display: block;
}
.accent-info-box {
padding: 12px 16px;
background: color-mix(in srgb, var(--accent) 10%, transparent);
border: 1px solid var(--accent);
border-radius: 8px;
margin-bottom: 12px;
}
`;
document.head.appendChild(style);
// === MODAL EVENTS ===
openBtn?.addEventListener('click', async function() {
modal.classList.add('show');
await checkPremium();
loadWorkflowsTab();
});
setupTabSwitching();
// Close on cancel
cancelBtn?.addEventListener('click', function() {
modal.classList.remove('show');
});
// Wire modal escape key / click outside
wireModal(modal, cancelBtn);
})();