395 lines
14 KiB
HTML
395 lines
14 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: 1400px;
|
||
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;
|
||
}
|
||
.grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(350px, 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; }
|
||
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;
|
||
}
|
||
.workflow-item, .execution-item, .worker-item {
|
||
padding: 15px;
|
||
border: 1px solid #e0e0e0;
|
||
border-radius: 5px;
|
||
margin-bottom: 10px;
|
||
background: #f9f9f9;
|
||
}
|
||
.workflow-item:hover, .execution-item:hover, .worker-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;
|
||
}
|
||
.loading {
|
||
text-align: center;
|
||
padding: 20px;
|
||
color: #666;
|
||
}
|
||
.empty {
|
||
text-align: center;
|
||
padding: 30px;
|
||
color: #999;
|
||
}
|
||
.timestamp {
|
||
font-size: 0.85em;
|
||
color: #999;
|
||
}
|
||
.error {
|
||
background: #fee2e2;
|
||
border: 2px solid #ef4444;
|
||
color: #991b1b;
|
||
padding: 20px;
|
||
border-radius: 10px;
|
||
margin-bottom: 20px;
|
||
}
|
||
</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 id="authError" class="error" style="display: none;">
|
||
<h3>⚠️ Authentication Required</h3>
|
||
<p>Please access PULSE through <strong>https://pulse.lotusguild.org</strong> to authenticate via Authelia.</p>
|
||
</div>
|
||
|
||
<div class="grid">
|
||
<div class="card">
|
||
<h3>👥 Workers</h3>
|
||
<div id="workerStatus">
|
||
<div class="loading">Loading...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>📋 Workflows</h3>
|
||
<div id="workflowList">
|
||
<div class="loading">Loading...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>🚀 Recent Executions</h3>
|
||
<div id="executionList">
|
||
<div class="loading">Loading...</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="card">
|
||
<h3>⚡ Quick Actions</h3>
|
||
<button onclick="refreshData()">🔄 Refresh Status</button>
|
||
<button onclick="alert('Workflow creation UI coming soon!')">➕ Create Workflow</button>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
let currentUser = null;
|
||
let ws = null;
|
||
|
||
async function loadUser() {
|
||
try {
|
||
const response = await fetch('/api/user');
|
||
if (!response.ok) {
|
||
document.getElementById('authError').style.display = 'block';
|
||
document.getElementById('userInfo').innerHTML =
|
||
'<div style="color: #ef4444;">⚠️ Not authenticated</div>';
|
||
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);
|
||
document.getElementById('authError').style.display = 'block';
|
||
document.getElementById('userInfo').innerHTML =
|
||
'<div style="color: #ef4444;">Error loading user</div>';
|
||
return false;
|
||
}
|
||
}
|
||
|
||
async function loadWorkers() {
|
||
try {
|
||
const response = await fetch('/api/workers');
|
||
const workers = await response.json();
|
||
|
||
if (workers.length === 0) {
|
||
document.getElementById('workerStatus').innerHTML =
|
||
'<div class="empty">No workers connected</div>';
|
||
return;
|
||
}
|
||
|
||
document.getElementById('workerStatus').innerHTML = 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('');
|
||
} catch (error) {
|
||
console.error('Error loading workers:', error);
|
||
document.getElementById('workerStatus').innerHTML =
|
||
'<div class="empty">Error loading workers</div>';
|
||
}
|
||
}
|
||
|
||
async function loadWorkflows() {
|
||
try {
|
||
const response = await fetch('/api/workflows');
|
||
const workflows = await response.json();
|
||
|
||
if (workflows.length === 0) {
|
||
document.getElementById('workflowList').innerHTML =
|
||
'<div class="empty">No workflows defined yet</div>';
|
||
return;
|
||
}
|
||
|
||
document.getElementById('workflowList').innerHTML = workflows.map(w => `
|
||
<div class="workflow-item">
|
||
<div class="workflow-name">${w.name}</div>
|
||
<div class="workflow-desc">${w.description || 'No description'}</div>
|
||
<div style="margin-top: 10px;">
|
||
<button onclick="executeWorkflow('${w.id}')">▶️ Execute</button>
|
||
${currentUser && currentUser.isAdmin ?
|
||
`<button class="danger" onclick="deleteWorkflow('${w.id}', '${w.name}')">🗑️ Delete</button>`
|
||
: ''}
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
} catch (error) {
|
||
console.error('Error loading workflows:', error);
|
||
document.getElementById('workflowList').innerHTML =
|
||
'<div class="empty">Error loading workflows</div>';
|
||
}
|
||
}
|
||
|
||
async function loadExecutions() {
|
||
try {
|
||
const response = await fetch('/api/executions');
|
||
const executions = await response.json();
|
||
|
||
if (executions.length === 0) {
|
||
document.getElementById('executionList').innerHTML =
|
||
'<div class="empty">No executions yet</div>';
|
||
return;
|
||
}
|
||
|
||
document.getElementById('executionList').innerHTML = executions.slice(0, 10).map(e => `
|
||
<div class="execution-item">
|
||
<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()}
|
||
</div>
|
||
</div>
|
||
`).join('');
|
||
} catch (error) {
|
||
console.error('Error loading executions:', error);
|
||
document.getElementById('executionList').innerHTML =
|
||
'<div class="empty">Error loading executions</div>';
|
||
}
|
||
}
|
||
|
||
async function executeWorkflow(workflowId) {
|
||
if (!confirm('Execute this workflow?')) return;
|
||
|
||
try {
|
||
const response = await fetch('/api/executions', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ workflow_id: workflowId })
|
||
});
|
||
|
||
if (response.ok) {
|
||
alert('Workflow execution started!');
|
||
loadExecutions();
|
||
} else {
|
||
alert('Failed to start workflow');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error executing workflow:', error);
|
||
alert('Error executing workflow');
|
||
}
|
||
}
|
||
|
||
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');
|
||
loadWorkflows();
|
||
} 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 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); // Refresh every 30 seconds
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|