455 lines
14 KiB
JavaScript
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);
|
|
})(); |