Initial PULSE server commit with Authelia SSO integration

This commit is contained in:
2025-11-29 19:26:20 -05:00
commit dcca2b9e50
5 changed files with 1922 additions and 0 deletions

394
public/index.html Normal file
View File

@ -0,0 +1,394 @@
<!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>