Files
pulse/public/index.html
Jared Vititoe b3806545bd Fix quick command executions not appearing in execution tab
Changes:
- Create execution record in database when quick command is sent
- Store initial log entry with command details
- Broadcast execution_started event to update UI
- Display quick commands as "[Quick Command]" in execution list
- Fix worker communication to properly track all executions

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 20:24:11 -05:00

1195 lines
45 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>PULSE - Workflow Orchestration</title>
<style>
:root {
/* Terminal Dark Backgrounds */
--bg-primary: #0a0a0a;
--bg-secondary: #1a1a1a;
--bg-tertiary: #2a2a2a;
/* Terminal Colors */
--terminal-green: #00ff41;
--terminal-amber: #ffb000;
--terminal-cyan: #00ffff;
--text-primary: #00ff41;
--text-secondary: #00cc33;
--text-muted: #008822;
/* Border & UI */
--border-color: #00ff41;
--shadow: none;
--hover-bg: rgba(0, 255, 65, 0.1);
/* Status Colors (adapted) */
--status-online: #28a745;
--status-offline: #dc3545;
--status-running: #ffc107;
--status-completed: #28a745;
--status-failed: #dc3545;
--status-waiting: #ffc107;
/* Terminal Font Stack */
--font-mono: 'Courier New', 'Consolas', 'Monaco', 'Menlo', monospace;
/* Glow Effects */
--glow-green: 0 0 5px #00ff41, 0 0 10px #00ff41, 0 0 15px #00ff41;
--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;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-mono);
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
padding: 20px;
position: relative;
animation: flicker 0.2s ease-in-out 30s infinite;
}
/* CRT Scanline Effect */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.15),
rgba(0, 0, 0, 0.15) 1px,
transparent 1px,
transparent 2px
);
pointer-events: none;
z-index: 9999;
animation: scanline 8s linear infinite;
}
@keyframes scanline {
0% { transform: translateY(0); }
100% { transform: translateY(4px); }
}
/* Data Stream Corner Effect */
body::after {
content: '10101010';
position: fixed;
bottom: 10px;
right: 10px;
font-family: var(--font-mono);
font-size: 0.6rem;
color: var(--terminal-green);
opacity: 0.1;
pointer-events: none;
letter-spacing: 2px;
animation: data-stream 3s linear infinite;
}
@keyframes data-stream {
0% { content: '10101010'; opacity: 0.1; }
25% { content: '01010101'; opacity: 0.15; }
50% { content: '11001100'; opacity: 0.1; }
75% { content: '00110011'; opacity: 0.15; }
100% { content: '10101010'; opacity: 0.1; }
}
@keyframes flicker {
0% { opacity: 1; }
10% { opacity: 0.95; }
20% { opacity: 1; }
30% { opacity: 0.97; }
40% { opacity: 1; }
}
.container { max-width: 1600px; margin: 0 auto; }
.header {
background: var(--bg-secondary);
padding: 20px 30px;
border: 2px solid var(--terminal-green);
border-radius: 0;
box-shadow: none;
margin-bottom: 30px;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
}
.header::before {
content: '╔';
position: absolute;
top: -2px;
left: -2px;
font-size: 1.5rem;
color: var(--terminal-green);
text-shadow: var(--glow-green);
line-height: 1;
z-index: 10;
}
.header::after {
content: '╗';
position: absolute;
top: -2px;
right: -2px;
font-size: 1.5rem;
color: var(--terminal-green);
text-shadow: var(--glow-green);
line-height: 1;
z-index: 10;
}
.header-left h1 {
color: var(--terminal-amber);
font-size: 2em;
margin-bottom: 5px;
text-shadow: var(--glow-amber-intense);
font-family: var(--font-mono);
}
.header-left h1::before {
content: '>> ';
color: var(--terminal-green);
}
.header-left p {
color: var(--terminal-green);
font-size: 1em;
font-family: var(--font-mono);
}
.user-info { text-align: right; }
.user-info .name {
font-weight: 600;
color: var(--terminal-green);
font-size: 1.1em;
font-family: var(--font-mono);
}
.user-info .email {
color: var(--text-secondary);
font-size: 0.9em;
font-family: var(--font-mono);
}
.user-info .badge {
display: inline-block;
background: transparent;
color: var(--terminal-amber);
padding: 4px 12px;
border: 2px solid var(--terminal-amber);
border-radius: 0;
font-size: 0.8em;
margin-top: 5px;
margin-left: 5px;
font-family: var(--font-mono);
}
.user-info .badge::before {
content: '[';
margin-right: 3px;
}
.user-info .badge::after {
content: ']';
margin-left: 3px;
}
.tabs {
background: var(--bg-secondary);
border: 2px solid var(--terminal-green);
border-radius: 0;
padding: 10px;
margin-bottom: 20px;
display: flex;
gap: 10px;
}
.tab {
padding: 12px 24px;
background: transparent;
border: 2px solid var(--terminal-green);
border-radius: 0;
cursor: pointer;
font-size: 1em;
font-weight: 600;
color: var(--terminal-green);
font-family: var(--font-mono);
transition: all 0.3s;
}
.tab.active {
background: rgba(0, 255, 65, 0.2);
color: var(--terminal-amber);
border-color: var(--terminal-amber);
text-shadow: var(--glow-amber);
}
.tab:hover {
background: rgba(0, 255, 65, 0.1);
color: var(--terminal-amber);
}
.tab.active:hover {
background: rgba(0, 255, 65, 0.25);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.card {
background: var(--bg-secondary);
padding: 25px;
border: 2px solid var(--terminal-green);
border-radius: 0;
box-shadow: none;
position: relative;
}
.card::before {
content: '┌';
position: absolute;
top: -2px;
left: -2px;
font-size: 1.2rem;
color: var(--terminal-green);
line-height: 1;
}
.card::after {
content: '┐';
position: absolute;
top: -2px;
right: -2px;
font-size: 1.2rem;
color: var(--terminal-green);
line-height: 1;
}
.card h3 {
color: var(--terminal-amber);
margin-bottom: 15px;
font-size: 1.2em;
font-family: var(--font-mono);
text-shadow: var(--glow-amber);
}
.card h3::before {
content: '═══ ';
color: var(--terminal-green);
}
.card h3::after {
content: ' ═══';
color: var(--terminal-green);
}
.status {
display: inline-block;
padding: 5px 15px;
border-radius: 0;
font-size: 0.9em;
font-weight: 600;
margin-bottom: 5px;
background: transparent;
border: 2px solid;
font-family: var(--font-mono);
}
.status.online {
border-color: var(--status-online);
color: var(--status-online);
text-shadow: 0 0 5px var(--status-online), 0 0 10px var(--status-online);
}
.status.online::before { content: '[●'; margin-right: 4px; }
.status.online::after { content: ']'; margin-left: 4px; }
.status.offline {
border-color: var(--status-offline);
color: var(--status-offline);
text-shadow: 0 0 5px var(--status-offline), 0 0 10px var(--status-offline);
}
.status.offline::before { content: '[○'; margin-right: 4px; }
.status.offline::after { content: ']'; margin-left: 4px; }
.status.running {
border-color: var(--status-running);
color: var(--status-running);
text-shadow: 0 0 5px var(--status-running), 0 0 10px var(--status-running);
}
.status.running::before {
content: '[◐';
margin-right: 4px;
animation: spin-status 2s linear infinite;
}
@keyframes spin-status {
0% { content: '[◐'; }
25% { content: '[◓'; }
50% { content: '[◑'; }
75% { content: '[◒'; }
100% { content: '[◐'; }
}
.status.running::after { content: ']'; margin-left: 4px; }
.status.completed {
border-color: var(--status-completed);
color: var(--status-completed);
text-shadow: 0 0 5px var(--status-completed), 0 0 10px var(--status-completed);
}
.status.completed::before { content: '[✓'; margin-right: 4px; }
.status.completed::after { content: ']'; margin-left: 4px; }
.status.failed {
border-color: var(--status-failed);
color: var(--status-failed);
text-shadow: 0 0 5px var(--status-failed), 0 0 10px var(--status-failed);
}
.status.failed::before { content: '[✗'; margin-right: 4px; }
.status.failed::after { content: ']'; margin-left: 4px; }
.status.waiting {
border-color: var(--status-waiting);
color: var(--status-waiting);
text-shadow: 0 0 5px var(--status-waiting), 0 0 10px var(--status-waiting);
}
.status.waiting::before { content: '[⏳'; margin-right: 4px; }
.status.waiting::after { content: ']'; margin-left: 4px; }
button {
background: transparent;
color: var(--terminal-green);
border: 2px solid var(--terminal-green);
border-radius: 0;
padding: 12px 24px;
cursor: pointer;
font-size: 1em;
font-weight: 600;
font-family: var(--font-mono);
text-transform: uppercase;
transition: all 0.3s ease;
margin-right: 10px;
margin-bottom: 10px;
}
button::before { content: '[ '; }
button::after { content: ' ]'; }
button:hover {
background: rgba(0, 255, 65, 0.15);
color: var(--terminal-amber);
border-color: var(--terminal-amber);
text-shadow: var(--glow-amber);
box-shadow: var(--glow-amber);
transform: translateY(-2px);
}
button.danger {
color: var(--status-failed);
border-color: var(--status-failed);
}
button.danger:hover {
background: rgba(220, 53, 69, 0.15);
text-shadow: 0 0 5px var(--status-failed), 0 0 10px var(--status-failed);
}
button.small {
padding: 6px 12px;
font-size: 0.85em;
}
.worker-item, .execution-item, .workflow-item {
padding: 15px;
border: 2px solid var(--terminal-green);
border-radius: 0;
margin-bottom: 10px;
background: var(--bg-secondary);
font-family: var(--font-mono);
transition: all 0.3s;
}
.worker-item:hover, .execution-item:hover, .workflow-item:hover {
background: rgba(0, 255, 65, 0.08);
box-shadow: inset 0 0 20px rgba(0, 255, 65, 0.1);
}
.workflow-name {
font-weight: 600;
color: var(--terminal-amber);
font-size: 1.1em;
margin-bottom: 5px;
font-family: var(--font-mono);
text-shadow: var(--glow-amber);
}
.workflow-name::before {
content: '> ';
color: var(--terminal-green);
}
.workflow-desc {
color: var(--terminal-green);
font-size: 0.9em;
margin-bottom: 10px;
font-family: var(--font-mono);
}
.loading {
text-align: center;
padding: 20px;
color: var(--terminal-green);
font-family: var(--font-mono);
}
.loading::after {
content: '...';
animation: loading-dots 1.5s steps(4, end) infinite;
}
@keyframes loading-dots {
0%, 20% { content: '.'; }
40% { content: '..'; }
60%, 100% { content: '...'; }
}
.empty {
text-align: center;
padding: 30px;
color: var(--text-muted);
font-family: var(--font-mono);
}
.empty::before {
content: '[ NO DATA ]';
display: block;
font-size: 1.2rem;
color: var(--terminal-green);
margin-bottom: 10px;
}
.timestamp {
font-size: 0.85em;
color: var(--text-muted);
font-family: var(--font-mono);
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.show { display: flex; }
.modal-content {
background: var(--bg-primary);
padding: 0;
border: 3px double var(--terminal-green);
border-radius: 0;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 0 30px rgba(0, 255, 65, 0.3);
position: relative;
}
.modal-content::before {
content: '╔';
position: absolute;
top: -3px;
left: -3px;
font-size: 1.5rem;
color: var(--terminal-green);
line-height: 1;
z-index: 10;
}
.modal-content::after {
content: '╗';
position: absolute;
top: -3px;
right: -3px;
font-size: 1.5rem;
color: var(--terminal-green);
line-height: 1;
z-index: 10;
}
.modal-content h2 {
margin: 0;
padding: 20px 30px;
background: var(--bg-secondary);
color: var(--terminal-amber);
border-bottom: 2px solid var(--terminal-green);
font-family: var(--font-mono);
text-shadow: var(--glow-amber);
}
.modal-content h2::before {
content: '═══ ';
color: var(--terminal-green);
}
.modal-content h2::after {
content: ' ═══';
color: var(--terminal-green);
}
input, textarea, select {
width: 100%;
padding: 12px;
margin-bottom: 15px;
border: 2px solid var(--terminal-green);
border-radius: 0;
font-size: 1em;
font-family: var(--font-mono);
background: var(--bg-primary);
color: var(--terminal-green);
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--terminal-amber);
box-shadow: var(--glow-amber), inset 0 0 10px rgba(0, 0, 0, 0.5);
background: rgba(0, 255, 65, 0.05);
}
textarea { min-height: 100px; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.log-entry {
padding: 10px;
background: var(--bg-secondary);
border-left: 3px solid var(--terminal-green);
margin-bottom: 10px;
font-family: var(--font-mono);
font-size: 0.9em;
color: var(--terminal-green);
}
.log-entry::before {
content: '> ';
color: var(--terminal-amber);
font-weight: bold;
}
.prompt-box {
background: rgba(255, 176, 0, 0.1);
border: 2px solid var(--terminal-amber);
padding: 20px;
border-radius: 0;
margin: 20px 0;
}
.prompt-box h3 {
color: var(--terminal-amber);
margin-bottom: 15px;
font-family: var(--font-mono);
text-shadow: var(--glow-amber);
}
.prompt-box h3::before {
content: '⏳ ';
}
/* Boot Overlay */
.boot-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--bg-primary);
z-index: 99999;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.5s;
}
#boot-text {
font-family: var(--font-mono);
color: var(--terminal-green);
text-shadow: var(--glow-green);
font-size: 0.95rem;
line-height: 1.6;
white-space: pre;
padding: 2rem;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<div class="header-left">
<h1>⚡ PULSE</h1>
<p>Pipelined Unified Logic & Server Engine</p>
</div>
<div class="user-info" id="userInfo">
<div class="loading">Loading user...</div>
</div>
</div>
<div class="tabs">
<button class="tab active" onclick="switchTab('dashboard')">📊 Dashboard</button>
<button class="tab" onclick="switchTab('workers')">👥 Workers</button>
<button class="tab" onclick="switchTab('workflows')">📋 Workflows</button>
<button class="tab" onclick="switchTab('executions')">🚀 Executions</button>
<button class="tab" onclick="switchTab('quickcommand')">⚡ Quick Command</button>
</div>
<!-- Dashboard Tab -->
<div id="dashboard" class="tab-content active">
<div class="grid">
<div class="card">
<h3>👥 Active Workers</h3>
<div id="dashWorkers"><div class="loading">Loading...</div></div>
</div>
<div class="card">
<h3>🚀 Recent Executions</h3>
<div id="dashExecutions"><div class="loading">Loading...</div></div>
</div>
</div>
</div>
<!-- Workers Tab -->
<div id="workers" class="tab-content">
<div class="card">
<h3>Worker Management</h3>
<button onclick="refreshData()">🔄 Refresh</button>
<div id="workerList"><div class="loading">Loading...</div></div>
</div>
</div>
<!-- Workflows Tab -->
<div id="workflows" class="tab-content">
<div class="card">
<h3>Workflow Management</h3>
<button onclick="showCreateWorkflow()"> Create Workflow</button>
<button onclick="refreshData()">🔄 Refresh</button>
<div id="workflowList"><div class="loading">Loading...</div></div>
</div>
</div>
<!-- Executions Tab -->
<div id="executions" class="tab-content">
<div class="card">
<h3>Execution History</h3>
<button onclick="refreshData()">🔄 Refresh</button>
<div id="executionList"><div class="loading">Loading...</div></div>
</div>
</div>
<!-- Quick Command Tab -->
<div id="quickcommand" class="tab-content">
<div class="card">
<h3>⚡ Quick Command Execution</h3>
<p style="color: #666; margin-bottom: 20px;">Execute a command on selected workers instantly</p>
<label style="display: block; margin-bottom: 10px; font-weight: 600;">Select Worker:</label>
<select id="quickWorkerSelect">
<option value="">Loading workers...</option>
</select>
<label style="display: block; margin-bottom: 10px; margin-top: 20px; font-weight: 600;">Command:</label>
<textarea id="quickCommand" placeholder="Enter command to execute (e.g., 'uptime' or 'df -h')"></textarea>
<button onclick="executeQuickCommand()">▶️ Execute Command</button>
<div id="quickCommandResult" style="margin-top: 20px;"></div>
</div>
</div>
</div>
<!-- Create Workflow Modal -->
<div id="createWorkflowModal" class="modal">
<div class="modal-content">
<h2>Create New Workflow</h2>
<input type="text" id="workflowName" placeholder="Workflow Name">
<textarea id="workflowDescription" placeholder="Description"></textarea>
<label>Workflow Definition (JSON):</label>
<textarea id="workflowDefinition" style="min-height: 200px;">{
"steps": [
{
"name": "Example Step",
"type": "execute",
"targets": ["all"],
"command": "echo 'Hello from PULSE'"
}
]
}</textarea>
<button onclick="createWorkflow()">Create</button>
<button onclick="closeModal('createWorkflowModal')">Cancel</button>
</div>
</div>
<!-- View Execution Modal -->
<div id="viewExecutionModal" class="modal">
<div class="modal-content">
<h2>Execution Details</h2>
<div id="executionDetails"></div>
<button onclick="closeModal('viewExecutionModal')">Close</button>
</div>
</div>
<script>
let currentUser = null;
let ws = null;
let workers = [];
async function loadUser() {
try {
const response = await fetch('/api/user');
if (!response.ok) return false;
currentUser = await response.json();
document.getElementById('userInfo').innerHTML = `
<div class="name">${currentUser.name}</div>
<div class="email">${currentUser.email}</div>
<div>${currentUser.groups.map(g =>
`<span class="badge">${g}</span>`
).join('')}</div>
`;
return true;
} catch (error) {
console.error('Error loading user:', error);
return false;
}
}
async function loadWorkers() {
try {
const response = await fetch('/api/workers');
workers = await response.json();
// Update worker select in quick command
const select = document.getElementById('quickWorkerSelect');
if (select) {
select.innerHTML = workers.map(w =>
`<option value="${w.id}">${w.name} (${w.status})</option>`
).join('');
}
// Dashboard view
const dashHtml = workers.length === 0 ?
'<div class="empty">No workers connected</div>' :
workers.map(w => `
<div class="worker-item">
<span class="status ${w.status}">${w.status}</span>
<strong>${w.name}</strong>
<div class="timestamp">Last seen: ${new Date(w.last_heartbeat).toLocaleString()}</div>
</div>
`).join('');
document.getElementById('dashWorkers').innerHTML = dashHtml;
// Full worker list
const fullHtml = workers.length === 0 ?
'<div class="empty">No workers connected</div>' :
workers.map(w => {
const meta = typeof w.metadata === 'string' ? JSON.parse(w.metadata) : w.metadata;
return `
<div class="worker-item">
<div style="display: flex; justify-content: space-between; align-items: start;">
<div>
<span class="status ${w.status}">${w.status}</span>
<strong>${w.name}</strong>
<div class="timestamp">ID: ${w.id}</div>
<div class="timestamp">Last heartbeat: ${new Date(w.last_heartbeat).toLocaleString()}</div>
${meta ? `
<div style="margin-top: 10px; font-size: 0.85em; color: #666;">
<div>CPUs: ${meta.cpus || 'N/A'} | RAM: ${meta.totalMem ? (meta.totalMem / 1024 / 1024 / 1024).toFixed(1) + 'GB' : 'N/A'}</div>
<div>Platform: ${meta.platform || 'N/A'} | Arch: ${meta.arch || 'N/A'}</div>
<div>Active Tasks: ${meta.activeTasks || 0}/${meta.maxConcurrentTasks || 0}</div>
</div>
` : ''}
</div>
${currentUser && currentUser.isAdmin ? `
<button class="danger small" onclick="deleteWorker('${w.id}', '${w.name}')">🗑️ Delete</button>
` : ''}
</div>
</div>
`;
}).join('');
document.getElementById('workerList').innerHTML = fullHtml;
} catch (error) {
console.error('Error loading workers:', error);
}
}
async function loadWorkflows() {
try {
const response = await fetch('/api/workflows');
const workflows = await response.json();
const html = workflows.length === 0 ?
'<div class="empty">No workflows defined yet</div>' :
workflows.map(w => `
<div class="workflow-item">
<div class="workflow-name">${w.name}</div>
<div class="workflow-desc">${w.description || 'No description'}</div>
<div class="timestamp">Created by ${w.created_by || 'Unknown'} on ${new Date(w.created_at).toLocaleString()}</div>
<div style="margin-top: 10px;">
<button onclick="executeWorkflow('${w.id}', '${w.name}')">▶️ Execute</button>
${currentUser && currentUser.isAdmin ?
`<button class="danger" onclick="deleteWorkflow('${w.id}', '${w.name}')">🗑️ Delete</button>`
: ''}
</div>
</div>
`).join('');
document.getElementById('workflowList').innerHTML = html;
} catch (error) {
console.error('Error loading workflows:', error);
}
}
async function loadExecutions() {
try {
const response = await fetch('/api/executions');
const executions = await response.json();
const dashHtml = executions.length === 0 ?
'<div class="empty">No executions yet</div>' :
executions.slice(0, 5).map(e => `
<div class="execution-item" onclick="viewExecution('${e.id}')">
<span class="status ${e.status}">${e.status}</span>
<strong>${e.workflow_name || '[Quick Command]'}</strong>
<div class="timestamp">by ${e.started_by} at ${new Date(e.started_at).toLocaleString()}</div>
</div>
`).join('');
document.getElementById('dashExecutions').innerHTML = dashHtml;
const fullHtml = executions.length === 0 ?
'<div class="empty">No executions yet</div>' :
executions.map(e => `
<div class="execution-item" onclick="viewExecution('${e.id}')">
<span class="status ${e.status}">${e.status}</span>
<strong>${e.workflow_name || '[Quick Command]'}</strong>
<div class="timestamp">
Started by ${e.started_by} at ${new Date(e.started_at).toLocaleString()}
${e.completed_at ? ` • Completed at ${new Date(e.completed_at).toLocaleString()}` : ''}
</div>
</div>
`).join('');
document.getElementById('executionList').innerHTML = fullHtml;
} catch (error) {
console.error('Error loading executions:', error);
}
}
async function executeWorkflow(workflowId, name) {
if (!confirm(`Execute workflow: ${name}?`)) return;
try {
const response = await fetch('/api/executions', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ workflow_id: workflowId })
});
if (response.ok) {
const data = await response.json();
alert('Workflow execution started!');
switchTab('executions');
refreshData();
} else {
alert('Failed to start workflow');
}
} catch (error) {
console.error('Error executing workflow:', error);
alert('Error executing workflow');
}
}
async function viewExecution(executionId) {
try {
const response = await fetch(`/api/executions/${executionId}`);
const execution = await response.json();
let html = `
<div><strong>Status:</strong> <span class="status ${execution.status}">${execution.status}</span></div>
<div><strong>Started:</strong> ${new Date(execution.started_at).toLocaleString()}</div>
${execution.completed_at ? `<div><strong>Completed:</strong> ${new Date(execution.completed_at).toLocaleString()}</div>` : ''}
<div><strong>Started by:</strong> ${execution.started_by}</div>
`;
if (execution.waiting_for_input && execution.prompt) {
html += `
<div class="prompt-box">
<h3>⏳ Waiting for Input</h3>
<p>${execution.prompt.message}</p>
<div style="margin-top: 15px;">
${execution.prompt.options.map(opt =>
`<button onclick="respondToPrompt('${executionId}', '${opt}')">${opt}</button>`
).join('')}
</div>
</div>
`;
}
if (execution.logs && execution.logs.length > 0) {
html += '<h3 style="margin-top: 20px; margin-bottom: 10px;">Execution Logs:</h3>';
execution.logs.forEach(log => {
html += `<div class="log-entry">${JSON.stringify(log, null, 2)}</div>`;
});
}
document.getElementById('executionDetails').innerHTML = html;
const modal = document.getElementById('viewExecutionModal');
modal.dataset.executionId = executionId;
modal.classList.add('show');
} catch (error) {
console.error('Error viewing execution:', error);
alert('Error loading execution details');
}
}
async function respondToPrompt(executionId, response) {
try {
const res = await fetch(`/api/executions/${executionId}/respond`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ response })
});
if (res.ok) {
closeModal('viewExecutionModal');
alert('Response submitted!');
refreshData();
} else {
alert('Failed to submit response');
}
} catch (error) {
console.error('Error responding to prompt:', error);
alert('Error submitting response');
}
}
async function deleteWorker(workerId, name) {
if (!confirm(`Delete worker: ${name}?`)) return;
try {
const response = await fetch(`/api/workers/${workerId}`, {
method: 'DELETE'
});
if (response.ok) {
alert('Worker deleted');
refreshData();
} else {
const data = await response.json();
alert(data.error || 'Failed to delete worker');
}
} catch (error) {
console.error('Error deleting worker:', error);
alert('Error deleting worker');
}
}
async function deleteWorkflow(workflowId, name) {
if (!confirm(`Delete workflow: ${name}? This cannot be undone.`)) return;
try {
const response = await fetch(`/api/workflows/${workflowId}`, {
method: 'DELETE'
});
if (response.ok) {
alert('Workflow deleted');
refreshData();
} else {
const data = await response.json();
alert(data.error || 'Failed to delete workflow');
}
} catch (error) {
console.error('Error deleting workflow:', error);
alert('Error deleting workflow');
}
}
function showCreateWorkflow() {
document.getElementById('createWorkflowModal').classList.add('show');
}
async function createWorkflow() {
const name = document.getElementById('workflowName').value;
const description = document.getElementById('workflowDescription').value;
const definitionText = document.getElementById('workflowDefinition').value;
if (!name || !definitionText) {
alert('Name and definition are required');
return;
}
try {
const definition = JSON.parse(definitionText);
const response = await fetch('/api/workflows', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, description, definition })
});
if (response.ok) {
alert('Workflow created!');
closeModal('createWorkflowModal');
switchTab('workflows');
refreshData();
} else {
alert('Failed to create workflow');
}
} catch (error) {
alert('Invalid JSON definition: ' + error.message);
}
}
async function executeQuickCommand() {
const workerId = document.getElementById('quickWorkerSelect').value;
const command = document.getElementById('quickCommand').value;
if (!workerId || !command) {
alert('Please select a worker and enter a command');
return;
}
const resultDiv = document.getElementById('quickCommandResult');
resultDiv.innerHTML = '<div class="loading">Executing command...</div>';
try {
const response = await fetch(`/api/workers/${workerId}/command`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command })
});
if (response.ok) {
const data = await response.json();
resultDiv.innerHTML = `
<div style="background: #f0fdf4; border: 2px solid #86efac; padding: 15px; border-radius: 5px;">
<strong style="color: #166534;">✓ Command sent successfully!</strong>
<div style="margin-top: 10px; font-family: monospace; font-size: 0.9em;">
Execution ID: ${data.execution_id}
</div>
<div style="margin-top: 10px; color: #166534;">
Check the Executions tab to see the results
</div>
</div>
`;
} else {
resultDiv.innerHTML = '<div style="color: #ef4444;">Failed to execute command</div>';
}
} catch (error) {
console.error('Error executing command:', error);
resultDiv.innerHTML = '<div style="color: #ef4444;">Error: ' + error.message + '</div>';
}
}
function closeModal(modalId) {
document.getElementById(modalId).classList.remove('show');
}
function switchTab(tabName) {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
event.target.classList.add('active');
document.getElementById(tabName).classList.add('active');
}
function refreshData() {
loadWorkers();
loadWorkflows();
loadExecutions();
}
function connectWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
ws = new WebSocket(`${protocol}//${window.location.host}`);
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('WebSocket message:', data);
// Handle specific message types
if (data.type === 'command_result') {
// Display command result in real-time
console.log(`Command result received for execution ${data.execution_id}`);
console.log(`Success: ${data.success}`);
console.log(`Output: ${data.stdout}`);
if (data.stderr) {
console.log(`Error: ${data.stderr}`);
}
// If viewing execution details, refresh that specific execution
const executionModal = document.getElementById('viewExecutionModal');
if (executionModal && executionModal.classList.contains('show')) {
// Reload execution details to show new logs
const executionId = executionModal.dataset.executionId;
if (executionId === data.execution_id) {
viewExecution(executionId);
}
}
// Refresh execution list to show updated status
loadExecutions();
}
if (data.type === 'workflow_result') {
console.log(`Workflow ${data.status} for execution ${data.execution_id}`);
// Refresh execution list
loadExecutions();
// If viewing this execution, refresh details
const executionModal = document.getElementById('viewExecutionModal');
if (executionModal && executionModal.classList.contains('show')) {
const executionId = executionModal.dataset.executionId;
if (executionId === data.execution_id) {
viewExecution(executionId);
}
}
}
if (data.type === 'worker_update') {
console.log(`Worker ${data.worker_id} status: ${data.status}`);
loadWorkers();
}
if (data.type === 'execution_started' || data.type === 'execution_status') {
loadExecutions();
}
if (data.type === 'workflow_created' || data.type === 'workflow_deleted') {
loadWorkflows();
}
// Generic refresh for other message types
if (!['command_result', 'workflow_result', 'worker_update', 'execution_started', 'execution_status', 'workflow_created', 'workflow_deleted'].includes(data.type)) {
refreshData();
}
};
ws.onclose = () => {
console.log('WebSocket closed, reconnecting...');
setTimeout(connectWebSocket, 5000);
};
}
// Initialize
loadUser().then((success) => {
if (success) {
refreshData();
connectWebSocket();
setInterval(refreshData, 30000);
}
});
</script>
<!-- Terminal Boot Sequence -->
<div id="boot-sequence" class="boot-overlay" style="display: none;">
<pre id="boot-text"></pre>
</div>
<script>
function showBootSequence() {
const bootText = document.getElementById('boot-text');
const bootOverlay = document.getElementById('boot-sequence');
bootOverlay.style.display = 'flex';
const messages = [
'╔═══════════════════════════════════════╗',
'║ PULSE ORCHESTRATION TERMINAL v1.0 ║',
'║ BOOTING SYSTEM... ║',
'╚═══════════════════════════════════════╝',
'',
'[ OK ] Loading kernel modules...',
'[ OK ] Initializing workflow engine...',
'[ OK ] Mounting worker connections...',
'[ OK ] Starting WebSocket services...',
'[ OK ] Rendering terminal interface...',
'',
'> SYSTEM READY ✓',
''
];
let i = 0;
const interval = setInterval(() => {
if (i < messages.length) {
bootText.textContent += messages[i] + '\n';
i++;
} else {
setTimeout(() => {
bootOverlay.style.opacity = '0';
setTimeout(() => {
bootOverlay.style.display = 'none';
}, 500);
}, 500);
clearInterval(interval);
}
}, 80);
}
// Run on first visit only (per session)
if (!sessionStorage.getItem('booted')) {
showBootSequence();
sessionStorage.setItem('booted', 'true');
}
</script>
</body>
</html>