diff --git a/public/index.html b/public/index.html index 8775a44..3fdfc84 100644 --- a/public/index.html +++ b/public/index.html @@ -15,6 +15,9 @@ --terminal-green: #00ff41; --terminal-amber: #ffb000; --terminal-cyan: #00ffff; + --terminal-red: #ff4444; + --bg-terminal: #001a00; + --bg-terminal-border: #003300; --text-primary: #00ff41; --text-secondary: #00cc33; --text-muted: #008822; @@ -40,6 +43,7 @@ --glow-green-intense: 0 0 8px #00ff41, 0 0 16px #00ff41, 0 0 24px #00ff41, 0 0 32px rgba(0, 255, 65, 0.5); --glow-amber: 0 0 5px #ffb000, 0 0 10px #ffb000, 0 0 15px #ffb000; --glow-amber-intense: 0 0 8px #ffb000, 0 0 16px #ffb000, 0 0 24px #ffb000; + --glow-red: 0 0 5px #ff4444, 0 0 10px #ff4444; } * { margin: 0; padding: 0; box-sizing: border-box; } @@ -72,6 +76,7 @@ pointer-events: none; z-index: 9999; animation: scanline 8s linear infinite; + will-change: transform; } @keyframes scanline { @@ -361,7 +366,12 @@ button::after { content: ' ]'; flex-shrink: 0; } /* Suppress bracket pseudo-elements for tab/nav buttons and inline-styled sub-tabs */ button.tab::before, button.tab::after, - button[style*="border:none"]::before, button[style*="border:none"]::after { content: none; } + button[style*="border:none"]::before, button[style*="border:none"]::after, + button[style*="border: none"]::before, button[style*="border: none"]::after, + button[style*="flex:0"]::before, button[style*="flex:0"]::after, + button[style*="flex: 0"]::before, button[style*="flex: 0"]::after, + button[style*="flex:1"]::before, button[style*="flex:1"]::after, + button[style*="flex: 1"]::before, button[style*="flex: 1"]::after { content: none; } button:hover { background: rgba(0, 255, 65, 0.15); color: var(--terminal-amber); @@ -466,7 +476,7 @@ border-radius: 0; max-width: 600px; width: 90%; - max-height: 80vh; + max-height: 85vh; overflow-y: auto; box-shadow: 0 0 30px rgba(0, 255, 65, 0.3); position: relative; @@ -624,7 +634,7 @@ } .log-entry.failed { - border-left-color: #ff4444; + border-left-color: var(--terminal-red); } .log-timestamp { @@ -642,8 +652,8 @@ } .log-entry.failed .log-title { - color: #ff4444; - text-shadow: 0 0 5px #ff4444; + color: var(--terminal-red); + text-shadow: var(--glow-red); } .log-details { @@ -662,8 +672,8 @@ } .log-output { - background: #0a0a0a; - border: 1px solid #003300; + background: var(--bg-primary); + border: 1px solid var(--bg-terminal-border); padding: 10px; margin: 6px 0; color: var(--terminal-green); @@ -675,14 +685,14 @@ } .log-error { - color: #ff6666; + color: var(--terminal-red); border-color: #330000; } .log-entry code { - background: #001a00; + background: var(--bg-terminal); padding: 2px 6px; - border: 1px solid #003300; + border: 1px solid var(--bg-terminal-border); color: var(--terminal-green); font-family: var(--font-mono); } @@ -699,15 +709,15 @@ .worker-stats span { padding: 2px 6px; - background: #001a00; - border: 1px solid #003300; + background: var(--bg-terminal); + border: 1px solid var(--bg-terminal-border); } .worker-metadata { margin-top: 12px; padding: 10px; - background: #001a00; - border: 1px solid #003300; + background: var(--bg-terminal); + border: 1px solid var(--bg-terminal-border); font-family: var(--font-mono); font-size: 0.85em; } @@ -743,18 +753,18 @@ } .execution-item:hover { - background: #001a00; + background: var(--bg-terminal); border-left-width: 5px; transform: translateX(3px); } .worker-item:hover { - background: #001a00; + background: var(--bg-terminal); border-left-width: 5px; } .workflow-item:hover { - background: #001a00; + background: var(--bg-terminal); border-left-width: 5px; } @@ -768,6 +778,15 @@ 50% { opacity: 1; } } + /* Running execution pulse */ + .execution-item.status-running { + animation: exec-running-pulse 2s ease-in-out infinite; + } + @keyframes exec-running-pulse { + 0%, 100% { border-color: var(--terminal-green); } + 50% { border-color: var(--status-running); box-shadow: 0 0 8px rgba(255,193,7,0.35); } + } + /* Success/Error message animations */ @keyframes slide-in { from { @@ -792,8 +811,11 @@

⚡ PULSE

Pipelined Unified Logic & Server Engine

-
-
Loading user...
+
+ +
@@ -1263,7 +1285,7 @@
${w.name}${paramBadge(def)}
${w.description || 'No description'}
-
Created by ${w.created_by || 'Unknown'} on ${new Date(w.created_at).toLocaleString()}
+
Created by ${w.created_by || 'Unknown'} on ${safeDate(w.created_at)?.toLocaleString() ?? 'N/A'}
${currentUser && currentUser.isAdmin ? @@ -1304,8 +1326,8 @@ scheduleDesc = `Cron: ${s.schedule_value}`; } - const nextRun = s.next_run ? new Date(s.next_run).toLocaleString() : 'Not scheduled'; - const lastRun = s.last_run ? new Date(s.last_run).toLocaleString() : 'Never'; + const nextRun = safeDate(s.next_run)?.toLocaleString() ?? 'Not scheduled'; + const lastRun = safeDate(s.last_run)?.toLocaleString() ?? 'Never'; return `
@@ -1497,7 +1519,7 @@
${e.status} ${e.workflow_name || '[Quick Command]'} -
by ${e.started_by} at ${new Date(e.started_at).toLocaleString()}
+
by ${e.started_by} at ${safeDate(e.started_at)?.toLocaleString() ?? 'N/A'}
`).join(''); document.getElementById('dashExecutions').innerHTML = dashHtml; @@ -1615,14 +1637,15 @@ const clickHandler = compareMode ? `toggleExecutionSelection('${e.id}')` : `viewExecution('${e.id}')`; const selectedStyle = isSelected ? 'background: rgba(255, 176, 0, 0.2); border-left-width: 5px; border-left-color: var(--terminal-amber);' : ''; + const elapsed = e.status === 'running' ? ` • ${formatElapsed(e.started_at)}` : ''; return ` -
+
${compareMode && isSelected ? '' : ''} ${e.status} ${e.workflow_name || '[Quick Command]'}
- Started by ${e.started_by} at ${new Date(e.started_at).toLocaleString()} - ${e.completed_at ? ` • Completed at ${new Date(e.completed_at).toLocaleString()}` : ''} + Started by ${e.started_by} at ${safeDate(e.started_at)?.toLocaleString() ?? 'N/A'} + ${e.completed_at ? ` • Completed at ${safeDate(e.completed_at)?.toLocaleString() ?? 'N/A'}` : elapsed}
`; @@ -1737,7 +1760,7 @@ Execution ${idx + 1} ${exec.status} - ${new Date(exec.started_at).toLocaleString()} + ${safeDate(exec.started_at)?.toLocaleString() ?? 'N/A'} ${duration} `; @@ -1853,24 +1876,14 @@ if (!confirm('Delete all completed and failed executions?')) return; try { - const response = await fetch('/api/executions?limit=9999'); // Get all executions - const data = await response.json(); - const executions = data.executions || data; // Handle new pagination format - - const toDelete = executions.filter(e => e.status === 'completed' || e.status === 'failed'); - - if (toDelete.length === 0) { - alert('No completed or failed executions to delete'); + const response = await fetch('/api/executions/completed', { method: 'DELETE' }); + if (!response.ok) { + const err = await response.json().catch(() => ({})); + showTerminalNotification(err.error || 'Failed to delete executions', 'error'); return; } - - let deleted = 0; - for (const execution of toDelete) { - const deleteResponse = await fetch(`/api/executions/${execution.id}`, { method: 'DELETE' }); - if (deleteResponse.ok) deleted++; - } - - showTerminalNotification(`Deleted ${deleted} execution(s)`, 'success'); + const data = await response.json(); + showTerminalNotification(`Deleted ${data.deleted} execution(s)`, 'success'); refreshData(); } catch (error) { console.error('Error clearing executions:', error); @@ -1970,8 +1983,8 @@ let html = `
Status: ${execution.status}
-
Started: ${new Date(execution.started_at).toLocaleString()}
- ${execution.completed_at ? `
Completed: ${new Date(execution.completed_at).toLocaleString()}
` : ''} +
Started: ${safeDate(execution.started_at)?.toLocaleString() ?? 'N/A'}
+ ${execution.completed_at ? `
Completed: ${safeDate(execution.completed_at)?.toLocaleString() ?? 'N/A'}
` : ''}
Started by: ${execution.started_by}
`; @@ -2015,7 +2028,7 @@ // Re-run button (only for quick commands with command in logs) const commandLog = execution.logs?.find(l => l.action === 'command_sent'); if (commandLog && commandLog.command) { - html += ``; + html += ``; } // Download logs button @@ -2264,8 +2277,9 @@ } function escapeHtml(text) { + if (text == null) return ''; const div = document.createElement('div'); - div.textContent = text; + div.textContent = String(text); return div.innerHTML; } @@ -2288,7 +2302,9 @@ } function getTimeAgo(date) { + if (!date || isNaN(date)) return 'N/A'; const seconds = Math.floor((new Date() - date) / 1000); + if (seconds < 0) return 'just now'; if (seconds < 60) return `${seconds}s ago`; const minutes = Math.floor(seconds / 60); if (minutes < 60) return `${minutes}m ago`; @@ -2298,6 +2314,22 @@ return `${days}d ago`; } + function safeDate(val) { + if (!val) return null; + const d = new Date(val); + return isNaN(d) ? null : d; + } + + function formatElapsed(startedAt) { + const start = safeDate(startedAt); + if (!start) return ''; + const secs = Math.floor((Date.now() - start) / 1000); + if (secs < 60) return `${secs}s`; + const mins = Math.floor(secs / 60); + if (mins < 60) return `${mins}m ${secs % 60}s`; + return `${Math.floor(mins / 60)}h ${mins % 60}m`; + } + async function rerunCommand(command, workerId) { if (!confirm(`Re-run command: ${command}?`)) return; @@ -2431,7 +2463,7 @@ const html = history.map((item, index) => `
${escapeHtml(item.command)}
-
${new Date(item.timestamp).toLocaleString()} - ${item.worker}
+
${safeDate(item.timestamp)?.toLocaleString() ?? 'N/A'} - ${item.worker}
`).join(''); document.getElementById('historyList').innerHTML = html; @@ -2800,37 +2832,24 @@ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active')); document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active')); - // Find the button by its onclick attribute rather than relying on bare `event` const tabBtn = document.querySelector(`.tab[onclick*="'${tabName}'"]`); if (tabBtn) tabBtn.classList.add('active'); const tabContent = document.getElementById(tabName); if (tabContent) tabContent.classList.add('active'); + + // Persist active tab across page loads + try { localStorage.setItem('pulse_activeTab', tabName); } catch {} } async function refreshData() { - try { - await loadWorkers(); - } catch (e) { - console.error('Error loading workers:', e); - } + try { await loadWorkers(); } catch (e) { console.error('Error loading workers:', e); } + try { await loadWorkflows(); } catch (e) { console.error('Error loading workflows:', e); } + try { await loadExecutions(); } catch (e) { console.error('Error loading executions:', e); } + try { await loadSchedules(); } catch (e) { console.error('Error loading schedules:', e); } - try { - await loadWorkflows(); - } catch (e) { - console.error('Error loading workflows:', e); - } - - try { - await loadExecutions(); - } catch (e) { - console.error('Error loading executions:', e); - } - - try { - await loadSchedules(); - } catch (e) { - console.error('Error loading schedules:', e); - } + // Update "last refreshed" indicator + const el = document.getElementById('lastRefreshed'); + if (el) el.textContent = `Refreshed: ${new Date().toLocaleTimeString()}`; } // Terminal beep sound (Web Audio API) @@ -2992,8 +3011,12 @@ loadExecutions(); } + if (data.type === 'executions_bulk_deleted') { + loadExecutions(); + } + // Generic refresh for other message types - if (!['command_result', 'workflow_result', 'worker_update', 'execution_started', 'execution_status', 'workflow_created', 'workflow_deleted', 'workflow_updated', 'execution_prompt'].includes(data.type)) { + if (!['command_result', 'workflow_result', 'worker_update', 'execution_started', 'execution_status', 'workflow_created', 'workflow_deleted', 'workflow_updated', 'execution_prompt', 'executions_bulk_deleted'].includes(data.type)) { refreshData(); } } catch (error) { @@ -3026,12 +3049,29 @@ // Initialize loadUser().then((success) => { if (success) { + // Restore last-active tab + try { + const saved = localStorage.getItem('pulse_activeTab'); + if (saved) switchTab(saved); + } catch {} + setExecutionView(executionView); refreshData(); connectWebSocket(); setInterval(refreshData, 30000); } }); + + // Ctrl+Enter submits the quick command form + document.addEventListener('keydown', (e) => { + if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { + const activeTab = document.querySelector('.tab-content.active'); + if (activeTab && activeTab.id === 'quickcommand') { + e.preventDefault(); + executeQuickCommand(); + } + } + });