Files
pulse/public/index.html
Jared Vititoe f8ec651e73 Phase 1: Improve log display formatting
Changes:
- Added formatLogEntry() function to parse and format log entries
- Replaced raw JSON display with readable formatted logs
- Added specific formatting for command_sent and command_result logs
- Show timestamp, status, duration, stdout/stderr in organized layout
- Color-coded success (green) and failure (red) states
- Added scrollable output sections with max-height
- Syntax highlighting for command code blocks
- Terminal-themed styling with green/amber colors

Benefits:
- Much easier to read execution logs
- Clear visual distinction between sent/result logs
- Professional terminal aesthetic maintained
- Better UX for debugging command execution

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-01-07 22:41:29 -05:00

1345 lines
50 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>
:root {
/* Terminal Dark Backgrounds */
--bg-primary: #0a0a0a;
--bg-secondary: #1a1a1a;
--bg-tertiary: #2a2a2a;
/* Terminal Colors */
--terminal-green: #00ff41;
--terminal-amber: #ffb000;
--terminal-cyan: #00ffff;
--text-primary: #00ff41;
--text-secondary: #00cc33;
--text-muted: #008822;
/* Border & UI */
--border-color: #00ff41;
--shadow: none;
--hover-bg: rgba(0, 255, 65, 0.1);
/* Status Colors (adapted) */
--status-online: #28a745;
--status-offline: #dc3545;
--status-running: #ffc107;
--status-completed: #28a745;
--status-failed: #dc3545;
--status-waiting: #ffc107;
/* Terminal Font Stack */
--font-mono: 'Courier New', 'Consolas', 'Monaco', 'Menlo', monospace;
/* Glow Effects */
--glow-green: 0 0 5px #00ff41, 0 0 10px #00ff41, 0 0 15px #00ff41;
--glow-green-intense: 0 0 8px #00ff41, 0 0 16px #00ff41, 0 0 24px #00ff41, 0 0 32px rgba(0, 255, 65, 0.5);
--glow-amber: 0 0 5px #ffb000, 0 0 10px #ffb000, 0 0 15px #ffb000;
--glow-amber-intense: 0 0 8px #ffb000, 0 0 16px #ffb000, 0 0 24px #ffb000;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-mono);
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
padding: 20px;
position: relative;
animation: flicker 0.2s ease-in-out 30s infinite;
}
/* CRT Scanline Effect */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: repeating-linear-gradient(
0deg,
rgba(0, 0, 0, 0.15),
rgba(0, 0, 0, 0.15) 1px,
transparent 1px,
transparent 2px
);
pointer-events: none;
z-index: 9999;
animation: scanline 8s linear infinite;
}
@keyframes scanline {
0% { transform: translateY(0); }
100% { transform: translateY(4px); }
}
/* Data Stream Corner Effect */
body::after {
content: '10101010';
position: fixed;
bottom: 10px;
right: 10px;
font-family: var(--font-mono);
font-size: 0.6rem;
color: var(--terminal-green);
opacity: 0.1;
pointer-events: none;
letter-spacing: 2px;
animation: data-stream 3s linear infinite;
}
@keyframes data-stream {
0% { content: '10101010'; opacity: 0.1; }
25% { content: '01010101'; opacity: 0.15; }
50% { content: '11001100'; opacity: 0.1; }
75% { content: '00110011'; opacity: 0.15; }
100% { content: '10101010'; opacity: 0.1; }
}
@keyframes flicker {
0% { opacity: 1; }
10% { opacity: 0.95; }
20% { opacity: 1; }
30% { opacity: 0.97; }
40% { opacity: 1; }
}
.container { max-width: 1600px; margin: 0 auto; }
.header {
background: var(--bg-secondary);
padding: 20px 30px;
border: 2px solid var(--terminal-green);
border-radius: 0;
box-shadow: none;
margin-bottom: 30px;
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
}
.header::before {
content: '╔';
position: absolute;
top: -2px;
left: -2px;
font-size: 1.5rem;
color: var(--terminal-green);
text-shadow: var(--glow-green);
line-height: 1;
z-index: 10;
}
.header::after {
content: '╗';
position: absolute;
top: -2px;
right: -2px;
font-size: 1.5rem;
color: var(--terminal-green);
text-shadow: var(--glow-green);
line-height: 1;
z-index: 10;
}
.header-left h1 {
color: var(--terminal-amber);
font-size: 2em;
margin-bottom: 5px;
text-shadow: var(--glow-amber-intense);
font-family: var(--font-mono);
}
.header-left h1::before {
content: '>> ';
color: var(--terminal-green);
}
.header-left p {
color: var(--terminal-green);
font-size: 1em;
font-family: var(--font-mono);
}
.user-info { text-align: right; }
.user-info .name {
font-weight: 600;
color: var(--terminal-green);
font-size: 1.1em;
font-family: var(--font-mono);
}
.user-info .email {
color: var(--text-secondary);
font-size: 0.9em;
font-family: var(--font-mono);
}
.user-info .badge {
display: inline-block;
background: transparent;
color: var(--terminal-amber);
padding: 4px 12px;
border: 2px solid var(--terminal-amber);
border-radius: 0;
font-size: 0.8em;
margin-top: 5px;
margin-left: 5px;
font-family: var(--font-mono);
}
.user-info .badge::before {
content: '[';
margin-right: 3px;
}
.user-info .badge::after {
content: ']';
margin-left: 3px;
}
.tabs {
background: var(--bg-secondary);
border: 2px solid var(--terminal-green);
border-radius: 0;
padding: 10px;
margin-bottom: 20px;
display: flex;
gap: 10px;
}
.tab {
padding: 12px 24px;
background: transparent;
border: 2px solid var(--terminal-green);
border-radius: 0;
cursor: pointer;
font-size: 1em;
font-weight: 600;
color: var(--terminal-green);
font-family: var(--font-mono);
transition: all 0.3s;
}
.tab.active {
background: rgba(0, 255, 65, 0.2);
color: var(--terminal-amber);
border-color: var(--terminal-amber);
text-shadow: var(--glow-amber);
}
.tab:hover {
background: rgba(0, 255, 65, 0.1);
color: var(--terminal-amber);
}
.tab.active:hover {
background: rgba(0, 255, 65, 0.25);
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(400px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.card {
background: var(--bg-secondary);
padding: 25px;
border: 2px solid var(--terminal-green);
border-radius: 0;
box-shadow: none;
position: relative;
}
.card::before {
content: '┌';
position: absolute;
top: -2px;
left: -2px;
font-size: 1.2rem;
color: var(--terminal-green);
line-height: 1;
}
.card::after {
content: '┐';
position: absolute;
top: -2px;
right: -2px;
font-size: 1.2rem;
color: var(--terminal-green);
line-height: 1;
}
.card h3 {
color: var(--terminal-amber);
margin-bottom: 15px;
font-size: 1.2em;
font-family: var(--font-mono);
text-shadow: var(--glow-amber);
}
.card h3::before {
content: '═══ ';
color: var(--terminal-green);
}
.card h3::after {
content: ' ═══';
color: var(--terminal-green);
}
.status {
display: inline-block;
padding: 5px 15px;
border-radius: 0;
font-size: 0.9em;
font-weight: 600;
margin-bottom: 5px;
background: transparent;
border: 2px solid;
font-family: var(--font-mono);
}
.status.online {
border-color: var(--status-online);
color: var(--status-online);
text-shadow: 0 0 5px var(--status-online), 0 0 10px var(--status-online);
}
.status.online::before { content: '[●'; margin-right: 4px; }
.status.online::after { content: ']'; margin-left: 4px; }
.status.offline {
border-color: var(--status-offline);
color: var(--status-offline);
text-shadow: 0 0 5px var(--status-offline), 0 0 10px var(--status-offline);
}
.status.offline::before { content: '[○'; margin-right: 4px; }
.status.offline::after { content: ']'; margin-left: 4px; }
.status.running {
border-color: var(--status-running);
color: var(--status-running);
text-shadow: 0 0 5px var(--status-running), 0 0 10px var(--status-running);
}
.status.running::before {
content: '[◐';
margin-right: 4px;
animation: spin-status 2s linear infinite;
}
@keyframes spin-status {
0% { content: '[◐'; }
25% { content: '[◓'; }
50% { content: '[◑'; }
75% { content: '[◒'; }
100% { content: '[◐'; }
}
.status.running::after { content: ']'; margin-left: 4px; }
.status.completed {
border-color: var(--status-completed);
color: var(--status-completed);
text-shadow: 0 0 5px var(--status-completed), 0 0 10px var(--status-completed);
}
.status.completed::before { content: '[✓'; margin-right: 4px; }
.status.completed::after { content: ']'; margin-left: 4px; }
.status.failed {
border-color: var(--status-failed);
color: var(--status-failed);
text-shadow: 0 0 5px var(--status-failed), 0 0 10px var(--status-failed);
}
.status.failed::before { content: '[✗'; margin-right: 4px; }
.status.failed::after { content: ']'; margin-left: 4px; }
.status.waiting {
border-color: var(--status-waiting);
color: var(--status-waiting);
text-shadow: 0 0 5px var(--status-waiting), 0 0 10px var(--status-waiting);
}
.status.waiting::before { content: '[⏳'; margin-right: 4px; }
.status.waiting::after { content: ']'; margin-left: 4px; }
button {
background: transparent;
color: var(--terminal-green);
border: 2px solid var(--terminal-green);
border-radius: 0;
padding: 12px 24px;
cursor: pointer;
font-size: 1em;
font-weight: 600;
font-family: var(--font-mono);
text-transform: uppercase;
transition: all 0.3s ease;
margin-right: 10px;
margin-bottom: 10px;
}
button::before { content: '[ '; }
button::after { content: ' ]'; }
button:hover {
background: rgba(0, 255, 65, 0.15);
color: var(--terminal-amber);
border-color: var(--terminal-amber);
text-shadow: var(--glow-amber);
box-shadow: var(--glow-amber);
transform: translateY(-2px);
}
button.danger {
color: var(--status-failed);
border-color: var(--status-failed);
}
button.danger:hover {
background: rgba(220, 53, 69, 0.15);
text-shadow: 0 0 5px var(--status-failed), 0 0 10px var(--status-failed);
}
button.small {
padding: 6px 12px;
font-size: 0.85em;
}
.worker-item, .execution-item, .workflow-item {
padding: 15px;
border: 2px solid var(--terminal-green);
border-radius: 0;
margin-bottom: 10px;
background: var(--bg-secondary);
font-family: var(--font-mono);
transition: all 0.3s;
}
.worker-item:hover, .execution-item:hover, .workflow-item:hover {
background: rgba(0, 255, 65, 0.08);
box-shadow: inset 0 0 20px rgba(0, 255, 65, 0.1);
}
.workflow-name {
font-weight: 600;
color: var(--terminal-amber);
font-size: 1.1em;
margin-bottom: 5px;
font-family: var(--font-mono);
text-shadow: var(--glow-amber);
}
.workflow-name::before {
content: '> ';
color: var(--terminal-green);
}
.workflow-desc {
color: var(--terminal-green);
font-size: 0.9em;
margin-bottom: 10px;
font-family: var(--font-mono);
}
.loading {
text-align: center;
padding: 20px;
color: var(--terminal-green);
font-family: var(--font-mono);
}
.loading::after {
content: '...';
animation: loading-dots 1.5s steps(4, end) infinite;
}
@keyframes loading-dots {
0%, 20% { content: '.'; }
40% { content: '..'; }
60%, 100% { content: '...'; }
}
.empty {
text-align: center;
padding: 30px;
color: var(--text-muted);
font-family: var(--font-mono);
}
.empty::before {
content: '[ NO DATA ]';
display: block;
font-size: 1.2rem;
color: var(--terminal-green);
margin-bottom: 10px;
}
.timestamp {
font-size: 0.85em;
color: var(--text-muted);
font-family: var(--font-mono);
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.85);
z-index: 1000;
align-items: center;
justify-content: center;
}
.modal.show { display: flex; }
.modal-content {
background: var(--bg-primary);
padding: 0;
border: 3px double var(--terminal-green);
border-radius: 0;
max-width: 600px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
box-shadow: 0 0 30px rgba(0, 255, 65, 0.3);
position: relative;
}
.modal-content::before {
content: '╔';
position: absolute;
top: -3px;
left: -3px;
font-size: 1.5rem;
color: var(--terminal-green);
line-height: 1;
z-index: 10;
}
.modal-content::after {
content: '╗';
position: absolute;
top: -3px;
right: -3px;
font-size: 1.5rem;
color: var(--terminal-green);
line-height: 1;
z-index: 10;
}
.modal-content h2 {
margin: 0;
padding: 20px 30px;
background: var(--bg-secondary);
color: var(--terminal-amber);
border-bottom: 2px solid var(--terminal-green);
font-family: var(--font-mono);
text-shadow: var(--glow-amber);
}
.modal-content h2::before {
content: '═══ ';
color: var(--terminal-green);
}
.modal-content h2::after {
content: ' ═══';
color: var(--terminal-green);
}
input, textarea, select {
width: 100%;
padding: 12px;
margin-bottom: 15px;
border: 2px solid var(--terminal-green);
border-radius: 0;
font-size: 1em;
font-family: var(--font-mono);
background: var(--bg-primary);
color: var(--terminal-green);
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.5);
}
input:focus, textarea:focus, select:focus {
outline: none;
border-color: var(--terminal-amber);
box-shadow: var(--glow-amber), inset 0 0 10px rgba(0, 0, 0, 0.5);
background: rgba(0, 255, 65, 0.05);
}
textarea { min-height: 100px; }
.tab-content { display: none; }
.tab-content.active { display: block; }
.log-entry {
padding: 10px;
background: var(--bg-secondary);
border-left: 3px solid var(--terminal-green);
margin-bottom: 10px;
font-family: var(--font-mono);
font-size: 0.9em;
color: var(--terminal-green);
}
.log-entry::before {
content: '> ';
color: var(--terminal-amber);
font-weight: bold;
}
.prompt-box {
background: rgba(255, 176, 0, 0.1);
border: 2px solid var(--terminal-amber);
padding: 20px;
border-radius: 0;
margin: 20px 0;
}
.prompt-box h3 {
color: var(--terminal-amber);
margin-bottom: 15px;
font-family: var(--font-mono);
text-shadow: var(--glow-amber);
}
.prompt-box h3::before {
content: '⏳ ';
}
/* Boot Overlay */
.boot-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: var(--bg-primary);
z-index: 99999;
display: flex;
align-items: center;
justify-content: center;
transition: opacity 0.5s;
}
#boot-text {
font-family: var(--font-mono);
color: var(--terminal-green);
text-shadow: var(--glow-green);
font-size: 0.95rem;
line-height: 1.6;
white-space: pre;
padding: 2rem;
}
/* Formatted Log Entry Styles */
.log-entry {
background: #000;
border: 1px solid var(--terminal-green);
border-left: 3px solid var(--terminal-green);
padding: 12px;
margin-bottom: 12px;
font-family: var(--font-mono);
font-size: 0.9em;
}
.log-entry.success {
border-left-color: var(--terminal-green);
}
.log-entry.failed {
border-left-color: #ff4444;
}
.log-timestamp {
color: var(--terminal-amber);
font-size: 0.85em;
margin-bottom: 6px;
}
.log-title {
color: var(--terminal-green);
font-weight: bold;
text-shadow: var(--glow-green);
margin-bottom: 8px;
font-size: 1.1em;
}
.log-entry.failed .log-title {
color: #ff4444;
text-shadow: 0 0 5px #ff4444;
}
.log-details {
margin-left: 20px;
}
.log-field {
margin: 6px 0;
color: var(--terminal-green);
}
.log-label {
color: var(--terminal-amber);
font-weight: bold;
margin-right: 8px;
}
.log-output {
background: #0a0a0a;
border: 1px solid #003300;
padding: 10px;
margin: 6px 0;
color: var(--terminal-green);
font-family: var(--font-mono);
font-size: 0.9em;
overflow-x: auto;
max-height: 300px;
overflow-y: auto;
}
.log-error {
color: #ff6666;
border-color: #330000;
}
.log-entry code {
background: #001a00;
padding: 2px 6px;
border: 1px solid #003300;
color: var(--terminal-green);
font-family: var(--font-mono);
}
</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>
<button onclick="clearCompletedExecutions()" style="margin-left: 10px;">[ 🗑️ Clear Completed ]</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 || '[Quick Command]'}</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 || '[Quick Command]'}</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 clearCompletedExecutions() {
if (!confirm('Delete all completed and failed executions?')) return;
try {
const response = await fetch('/api/executions');
const executions = await response.json();
const toDelete = executions.filter(e => e.status === 'completed' || e.status === 'failed');
if (toDelete.length === 0) {
alert('No completed or failed executions to delete');
return;
}
for (const execution of toDelete) {
await fetch(`/api/executions/${execution.id}`, { method: 'DELETE' });
}
alert(`Deleted ${toDelete.length} execution(s)`);
refreshData();
} catch (error) {
console.error('Error clearing executions:', error);
alert('Error clearing executions');
}
}
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 += formatLogEntry(log);
});
}
document.getElementById('executionDetails').innerHTML = html;
const modal = document.getElementById('viewExecutionModal');
modal.dataset.executionId = executionId;
modal.classList.add('show');
} catch (error) {
console.error('Error viewing execution:', error);
alert('Error loading execution details');
}
}
function formatLogEntry(log) {
const timestamp = new Date(log.timestamp).toLocaleTimeString();
// Format based on log action type
if (log.action === 'command_sent') {
return `
<div class="log-entry log-command-sent">
<div class="log-timestamp">[${timestamp}]</div>
<div class="log-title">Command Sent</div>
<div class="log-details">
<div class="log-field"><span class="log-label">Command:</span> <code>${escapeHtml(log.command)}</code></div>
</div>
</div>
`;
}
if (log.action === 'command_result') {
const statusIcon = log.success ? '✓' : '✗';
const statusClass = log.success ? 'success' : 'failed';
return `
<div class="log-entry log-command-result ${statusClass}">
<div class="log-timestamp">[${timestamp}]</div>
<div class="log-title">${statusIcon} Command Result</div>
<div class="log-details">
<div class="log-field"><span class="log-label">Status:</span> ${log.success ? 'Success' : 'Failed'}</div>
${log.duration ? `<div class="log-field"><span class="log-label">Duration:</span> ${log.duration}ms</div>` : ''}
${log.stdout ? `<div class="log-field"><span class="log-label">Output:</span><pre class="log-output">${escapeHtml(log.stdout)}</pre></div>` : ''}
${log.stderr ? `<div class="log-field"><span class="log-label">Errors:</span><pre class="log-output log-error">${escapeHtml(log.stderr)}</pre></div>` : ''}
${log.error ? `<div class="log-field"><span class="log-label">Error:</span> ${escapeHtml(log.error)}</div>` : ''}
</div>
</div>
`;
}
// Fallback for unknown log types
return `<div class="log-entry"><pre>${JSON.stringify(log, null, 2)}</pre></div>`;
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
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);
// Handle specific message types
if (data.type === 'command_result') {
// Display command result in real-time
console.log(`Command result received for execution ${data.execution_id}`);
console.log(`Success: ${data.success}`);
console.log(`Output: ${data.stdout}`);
if (data.stderr) {
console.log(`Error: ${data.stderr}`);
}
// If viewing execution details, refresh that specific execution
const executionModal = document.getElementById('viewExecutionModal');
if (executionModal && executionModal.classList.contains('show')) {
// Reload execution details to show new logs
const executionId = executionModal.dataset.executionId;
if (executionId === data.execution_id) {
viewExecution(executionId);
}
}
// Refresh execution list to show updated status
loadExecutions();
}
if (data.type === 'workflow_result') {
console.log(`Workflow ${data.status} for execution ${data.execution_id}`);
// Refresh execution list
loadExecutions();
// If viewing this execution, refresh details
const executionModal = document.getElementById('viewExecutionModal');
if (executionModal && executionModal.classList.contains('show')) {
const executionId = executionModal.dataset.executionId;
if (executionId === data.execution_id) {
viewExecution(executionId);
}
}
}
if (data.type === 'worker_update') {
console.log(`Worker ${data.worker_id} status: ${data.status}`);
loadWorkers();
}
if (data.type === 'execution_started' || data.type === 'execution_status') {
loadExecutions();
}
if (data.type === 'workflow_created' || data.type === 'workflow_deleted') {
loadWorkflows();
}
// Generic refresh for other message types
if (!['command_result', 'workflow_result', 'worker_update', 'execution_started', 'execution_status', 'workflow_created', 'workflow_deleted'].includes(data.type)) {
refreshData();
}
};
ws.onclose = () => {
console.log('WebSocket closed, reconnecting...');
setTimeout(connectWebSocket, 5000);
};
}
// Initialize
loadUser().then((success) => {
if (success) {
refreshData();
connectWebSocket();
setInterval(refreshData, 30000);
}
});
</script>
<!-- Terminal Boot Sequence -->
<div id="boot-sequence" class="boot-overlay" style="display: none;">
<pre id="boot-text"></pre>
</div>
<script>
function showBootSequence() {
const bootText = document.getElementById('boot-text');
const bootOverlay = document.getElementById('boot-sequence');
bootOverlay.style.display = 'flex';
const messages = [
'╔═══════════════════════════════════════╗',
'║ PULSE ORCHESTRATION TERMINAL v1.0 ║',
'║ BOOTING SYSTEM... ║',
'╚═══════════════════════════════════════╝',
'',
'[ OK ] Loading kernel modules...',
'[ OK ] Initializing workflow engine...',
'[ OK ] Mounting worker connections...',
'[ OK ] Starting WebSocket services...',
'[ OK ] Rendering terminal interface...',
'',
'> SYSTEM READY ✓',
''
];
let i = 0;
const interval = setInterval(() => {
if (i < messages.length) {
bootText.textContent += messages[i] + '\n';
i++;
} else {
setTimeout(() => {
bootOverlay.style.opacity = '0';
setTimeout(() => {
bootOverlay.style.display = 'none';
}, 500);
}, 500);
clearInterval(interval);
}
}, 80);
}
// Run on first visit only (per session)
if (!sessionStorage.getItem('booted')) {
showBootSequence();
sessionStorage.setItem('booted', 'true');
}
</script>
</body>
</html>