696 lines
28 KiB
HTML
696 lines
28 KiB
HTML
<!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> |