diff --git a/public/index.html b/public/index.html index e75f9eb..9c34e3b 100644 --- a/public/index.html +++ b/public/index.html @@ -969,22 +969,32 @@ } } - async function loadExecutions() { - try { - const response = await fetch('/api/executions'); - const executions = await response.json(); - - const dashHtml = executions.length === 0 ? - '
No executions yet
' : - executions.slice(0, 5).map(e => ` -
- ${e.status} - ${e.workflow_name || '[Quick Command]'} -
by ${e.started_by} at ${new Date(e.started_at).toLocaleString()}
-
- `).join(''); - document.getElementById('dashExecutions').innerHTML = dashHtml; + let executionOffset = 0; + const executionLimit = 50; + async function loadExecutions(append = false) { + try { + if (!append) executionOffset = 0; + + const response = await fetch(`/api/executions?limit=${executionLimit}&offset=${executionOffset}`); + const data = await response.json(); + const executions = data.executions || data; // Handle old and new API format + + // Dashboard view (always first 5) + if (!append) { + const dashHtml = executions.length === 0 ? + '
No executions yet
' : + executions.slice(0, 5).map(e => ` +
+ ${e.status} + ${e.workflow_name || '[Quick Command]'} +
by ${e.started_by} at ${new Date(e.started_at).toLocaleString()}
+
+ `).join(''); + document.getElementById('dashExecutions').innerHTML = dashHtml; + } + + // Full execution list const fullHtml = executions.length === 0 ? '
No executions yet
' : executions.map(e => ` @@ -997,12 +1007,29 @@ `).join(''); - document.getElementById('executionList').innerHTML = fullHtml; + + if (append) { + document.getElementById('executionList').innerHTML += fullHtml; + } else { + document.getElementById('executionList').innerHTML = fullHtml; + } + + // Add "Load More" button if there are more executions + if (data.hasMore) { + const loadMoreBtn = ``; + document.getElementById('executionList').innerHTML += loadMoreBtn; + } + } catch (error) { console.error('Error loading executions:', error); } } + async function loadMoreExecutions() { + executionOffset += executionLimit; + await loadExecutions(true); + } + async function clearCompletedExecutions() { if (!confirm('Delete all completed and failed executions?')) return; diff --git a/server.js b/server.js index bd0bce4..80032c1 100644 --- a/server.js +++ b/server.js @@ -98,6 +98,27 @@ async function initDatabase() { } } +// Auto-cleanup old executions (runs daily) +async function cleanupOldExecutions() { + try { + const retentionDays = parseInt(process.env.EXECUTION_RETENTION_DAYS) || 30; + const [result] = await pool.query( + `DELETE FROM executions + WHERE status IN ('completed', 'failed') + AND started_at < DATE_SUB(NOW(), INTERVAL ? DAY)`, + [retentionDays] + ); + console.log(`[Cleanup] Removed ${result.affectedRows} executions older than ${retentionDays} days`); + } catch (error) { + console.error('[Cleanup] Error removing old executions:', error); + } +} + +// Run cleanup daily at 3 AM +setInterval(cleanupOldExecutions, 24 * 60 * 60 * 1000); +// Run cleanup on startup +cleanupOldExecutions(); + // WebSocket connections const clients = new Set(); const workers = new Map(); // Map worker_id -> WebSocket connection @@ -409,10 +430,25 @@ app.post('/api/executions', authenticateSSO, async (req, res) => { app.get('/api/executions', authenticateSSO, async (req, res) => { try { + const limit = parseInt(req.query.limit) || 50; + const offset = parseInt(req.query.offset) || 0; + const [rows] = await pool.query( - 'SELECT e.*, w.name as workflow_name FROM executions e LEFT JOIN workflows w ON e.workflow_id = w.id ORDER BY e.started_at DESC LIMIT 50' + 'SELECT e.*, w.name as workflow_name FROM executions e LEFT JOIN workflows w ON e.workflow_id = w.id ORDER BY e.started_at DESC LIMIT ? OFFSET ?', + [limit, offset] ); - res.json(rows); + + // Get total count + const [countRows] = await pool.query('SELECT COUNT(*) as total FROM executions'); + const total = countRows[0].total; + + res.json({ + executions: rows, + total: total, + limit: limit, + offset: offset, + hasMore: offset + rows.length < total + }); } catch (error) { res.status(500).json({ error: error.message }); }