Files
pulse/public/index.html

696 lines
28 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>
* { 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;
}
.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;
}
.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;
}
.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;
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);
}
.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; }
.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);
}
button.danger { background: #ef4444; }
button.danger:hover { background: #dc2626; }
button.small {
padding: 6px 12px;
font-size: 0.85em;
}
.worker-item, .execution-item, .workflow-item {
padding: 15px;
border: 1px solid #e0e0e0;
border-radius: 5px;
margin-bottom: 10px;
background: #f9f9f9;
}
.worker-item:hover, .execution-item:hover, .workflow-item:hover {
background: #f0f0f0;
}
.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;
}
.modal.show { display: flex; }
.modal-content {
background: white;
padding: 30px;
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;
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: #667eea;
}
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;
margin: 20px 0;
}
.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>
<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 || '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>
`).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;
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) {
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);
refreshData();
};
ws.onclose = () => {
console.log('WebSocket closed, reconnecting...');
setTimeout(connectWebSocket, 5000);
};
}
// Initialize
loadUser().then((success) => {
if (success) {
refreshData();
connectWebSocket();
setInterval(refreshData, 30000);
}
});
</script>
</body>
</html>