Files
pulse/public/index.html

696 lines
28 KiB
HTML
Raw Permalink Normal View History

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
2025-11-30 13:03:18 -05:00
<title>PULSE - Workflow Orchestration</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
2025-11-30 13:03:18 -05:00
.container { max-width: 1600px; margin: 0 auto; }
.header {
background: white;
padding: 30px;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
margin-bottom: 30px;
display: flex;
justify-content: space-between;
align-items: center;
}
2025-11-30 13:03:18 -05:00
.header-left h1 { color: #667eea; font-size: 2.5em; margin-bottom: 5px; }
.header-left p { color: #666; font-size: 1.1em; }
.user-info { text-align: right; }
.user-info .name { font-weight: 600; color: #333; font-size: 1.1em; }
.user-info .email { color: #666; font-size: 0.9em; }
.user-info .badge {
display: inline-block;
background: #667eea;
color: white;
padding: 4px 12px;
border-radius: 12px;
font-size: 0.8em;
margin-top: 5px;
margin-left: 5px;
}
2025-11-30 13:03:18 -05:00
.tabs {
background: white;
border-radius: 10px;
padding: 10px;
margin-bottom: 20px;
display: flex;
gap: 10px;
}
.tab {
padding: 12px 24px;
background: transparent;
border: none;
cursor: pointer;
font-size: 1em;
font-weight: 600;
color: #666;
border-radius: 5px;
transition: all 0.3s;
}
.tab.active {
background: #667eea;
color: white;
}
.tab:hover { background: #f0f0f0; }
.tab.active:hover { background: #5568d3; }
.grid {
display: grid;
2025-11-30 13:03:18 -05:00
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.card {
background: white;
padding: 25px;
border-radius: 10px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
}
2025-11-30 13:03:18 -05:00
.card h3 { color: #333; margin-bottom: 15px; font-size: 1.3em; }
.status {
display: inline-block;
padding: 5px 15px;
border-radius: 20px;
font-size: 0.9em;
font-weight: 600;
margin-bottom: 5px;
}
.status.online { background: #10b981; color: white; }
.status.offline { background: #ef4444; color: white; }
.status.running { background: #3b82f6; color: white; }
.status.completed { background: #10b981; color: white; }
.status.failed { background: #ef4444; color: white; }
2025-11-30 13:03:18 -05:00
.status.waiting { background: #f59e0b; color: white; }
button {
background: #667eea;
color: white;
border: none;
padding: 12px 24px;
border-radius: 5px;
cursor: pointer;
font-size: 1em;
font-weight: 600;
transition: all 0.3s;
margin-right: 10px;
margin-bottom: 10px;
}
button:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
}
2025-11-30 13:03:18 -05:00
button.danger { background: #ef4444; }
button.danger:hover { background: #dc2626; }
button.small {
padding: 6px 12px;
font-size: 0.85em;
}
2025-11-30 13:03:18 -05:00
.worker-item, .execution-item, .workflow-item {
padding: 15px;
border: 1px solid #e0e0e0;
border-radius: 5px;
margin-bottom: 10px;
background: #f9f9f9;
}
2025-11-30 13:03:18 -05:00
.worker-item:hover, .execution-item:hover, .workflow-item:hover {
background: #f0f0f0;
}
2025-11-30 13:03:18 -05:00
.workflow-name { font-weight: 600; color: #333; font-size: 1.1em; margin-bottom: 5px; }
.workflow-desc { color: #666; font-size: 0.9em; margin-bottom: 10px; }
.loading { text-align: center; padding: 20px; color: #666; }
.empty { text-align: center; padding: 30px; color: #999; }
.timestamp { font-size: 0.85em; color: #999; }
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.5);
z-index: 1000;
align-items: center;
justify-content: center;
}
2025-11-30 13:03:18 -05:00
.modal.show { display: flex; }
.modal-content {
background: white;
padding: 30px;
2025-11-30 13:03:18 -05:00
border-radius: 10px;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.modal-content h2 { margin-bottom: 20px; color: #333; }
input, textarea, select {
width: 100%;
padding: 12px;
margin-bottom: 15px;
border: 2px solid #e0e0e0;
border-radius: 5px;
font-size: 1em;
font-family: inherit;
}
2025-11-30 13:03:18 -05:00
input:focus, textarea:focus, select:focus {
outline: none;
border-color: #667eea;
}
2025-11-30 13:03:18 -05:00
textarea { min-height: 100px; font-family: monospace; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.log-entry {
padding: 10px;
background: #f9f9f9;
border-left: 3px solid #667eea;
margin-bottom: 10px;
font-family: monospace;
font-size: 0.9em;
}
.prompt-box {
background: #fef3c7;
border: 2px solid #f59e0b;
padding: 20px;
border-radius: 10px;
2025-11-30 13:03:18 -05:00
margin: 20px 0;
}
2025-11-30 13:03:18 -05:00
.prompt-box h3 { color: #92400e; margin-bottom: 15px; }
</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>
2025-11-30 13:03:18 -05:00
<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>
2025-11-30 13:03:18 -05:00
<!-- 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>
2025-11-30 13:03:18 -05:00
</div>
2025-11-30 13:03:18 -05:00
<!-- Workers Tab -->
<div id="workers" class="tab-content">
<div class="card">
2025-11-30 13:03:18 -05:00
<h3>Worker Management</h3>
<button onclick="refreshData()">🔄 Refresh</button>
<div id="workerList"><div class="loading">Loading...</div></div>
</div>
2025-11-30 13:03:18 -05:00
</div>
2025-11-30 13:03:18 -05:00
<!-- Workflows Tab -->
<div id="workflows" class="tab-content">
<div class="card">
2025-11-30 13:03:18 -05:00
<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>
2025-11-30 13:03:18 -05:00
<!-- 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;
2025-11-30 13:03:18 -05:00
let workers = [];
async function loadUser() {
try {
const response = await fetch('/api/user');
2025-11-30 13:03:18 -05:00
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');
2025-11-30 13:03:18 -05:00
workers = await response.json();
2025-11-30 13:03:18 -05:00
// 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('');
}
2025-11-30 13:03:18 -05:00
// 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();
2025-11-30 13:03:18 -05:00
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>
2025-11-30 13:03:18 -05:00
`).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();
2025-11-30 13:03:18 -05:00
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 || 'Unknown Workflow'}</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 || 'Unknown Workflow'}</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>
2025-11-30 13:03:18 -05:00
`).join('');
document.getElementById('executionList').innerHTML = fullHtml;
} catch (error) {
console.error('Error loading executions:', error);
}
}
2025-11-30 13:03:18 -05:00
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) {
2025-11-30 13:03:18 -05:00
const data = await response.json();
alert('Workflow execution started!');
2025-11-30 13:03:18 -05:00
switchTab('executions');
refreshData();
} else {
alert('Failed to start workflow');
}
} catch (error) {
console.error('Error executing workflow:', error);
alert('Error executing workflow');
}
}
2025-11-30 13:03:18 -05:00
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;
document.getElementById('viewExecutionModal').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) {
2025-11-30 13:03:18 -05:00
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');
2025-11-30 13:03:18 -05:00
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');
}
}
2025-11-30 13:03:18 -05:00
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);
refreshData();
};
ws.onclose = () => {
console.log('WebSocket closed, reconnecting...');
setTimeout(connectWebSocket, 5000);
};
}
// Initialize
loadUser().then((success) => {
if (success) {
refreshData();
connectWebSocket();
2025-11-30 13:03:18 -05:00
setInterval(refreshData, 30000);
}
});
</script>
</body>
2025-11-30 13:03:18 -05:00
</html>