Files
pulse/public/index.html

395 lines
14 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: 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>