feat: Gandalf M2M API, manual/automated execution sub-tabs, cleanup tuning
- server.js: add authenticateGandalf middleware (X-Gandalf-API-Key header)
and two internal endpoints used by Gandalf link diagnostics:
POST /api/internal/command — submit SSH command to a worker, returns execution_id
GET /api/internal/executions/:id — poll execution status/logs
Also tag automated executions as started_by 'gandalf:*' / 'scheduler:*';
add hide_internal query param to GET /api/executions; change cleanup
from daily/30d to hourly/1d to keep execution history lean
- index.html: add Manual / Automated sub-tabs on Execution History tab so
Gandalf diagnostic runs don't clutter the manual run view; persists
selected tab to localStorage; dashboard recent-run strip filters to
manual runs only; sub-tabs show live counts
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -811,6 +811,20 @@
|
||||
<div class="card">
|
||||
<h3>Execution History</h3>
|
||||
|
||||
<!-- Manual / Automated sub-tabs -->
|
||||
<div style="display: flex; gap: 0; margin-bottom: 20px; border: 2px solid var(--terminal-green);">
|
||||
<button id="subTabManual"
|
||||
onclick="setExecutionView('manual')"
|
||||
style="flex:1; padding:10px 16px; background:rgba(0,255,65,0.2); border:none; border-right:2px solid var(--terminal-green); color:var(--terminal-amber); font-family:var(--font-mono); font-size:0.9em; cursor:pointer; text-shadow: 0 0 5px #ffb000;">
|
||||
[ 👤 Manual Runs <span id="countManual"></span>]
|
||||
</button>
|
||||
<button id="subTabAutomated"
|
||||
onclick="setExecutionView('automated')"
|
||||
style="flex:1; padding:10px 16px; background:transparent; border:none; color:var(--terminal-green); font-family:var(--font-mono); font-size:0.9em; cursor:pointer;">
|
||||
[ 🤖 Automated <span id="countAutomated"></span>]
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Search and Filter Section -->
|
||||
<div style="background: rgba(0, 255, 65, 0.05); border: 2px solid var(--terminal-green); padding: 15px; margin-bottom: 20px;">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 15px; margin-bottom: 15px;">
|
||||
@@ -1358,6 +1372,7 @@
|
||||
|
||||
let executionOffset = 0;
|
||||
const executionLimit = 50;
|
||||
let executionView = localStorage.getItem('pulse_executionView') || 'manual';
|
||||
|
||||
async function loadExecutions(append = false) {
|
||||
try {
|
||||
@@ -1374,11 +1389,12 @@
|
||||
allExecutions = executions;
|
||||
}
|
||||
|
||||
// Dashboard view (always first 5)
|
||||
// Dashboard view (first 5 manual runs only)
|
||||
if (!append) {
|
||||
const dashHtml = executions.length === 0 ?
|
||||
const manualExecs = executions.filter(e => !isAutomatedRun(e));
|
||||
const dashHtml = manualExecs.length === 0 ?
|
||||
'<div class="empty">No executions yet</div>' :
|
||||
executions.slice(0, 5).map(e => `
|
||||
manualExecs.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>
|
||||
@@ -1402,12 +1418,57 @@
|
||||
}
|
||||
}
|
||||
|
||||
function isAutomatedRun(e) {
|
||||
const by = e.started_by || '';
|
||||
return by.startsWith('gandalf:') || by.startsWith('scheduler:');
|
||||
}
|
||||
|
||||
function updateSubTabCounts() {
|
||||
const manual = allExecutions.filter(e => !isAutomatedRun(e)).length;
|
||||
const automated = allExecutions.filter(e => isAutomatedRun(e)).length;
|
||||
const cm = document.getElementById('countManual');
|
||||
const ca = document.getElementById('countAutomated');
|
||||
if (cm) cm.textContent = manual ? `(${manual}) ` : '';
|
||||
if (ca) ca.textContent = automated ? `(${automated}) ` : '';
|
||||
}
|
||||
|
||||
function setExecutionView(view) {
|
||||
executionView = view;
|
||||
localStorage.setItem('pulse_executionView', view);
|
||||
const manualBtn = document.getElementById('subTabManual');
|
||||
const autoBtn = document.getElementById('subTabAutomated');
|
||||
if (manualBtn && autoBtn) {
|
||||
if (view === 'manual') {
|
||||
manualBtn.style.background = 'rgba(0,255,65,0.2)';
|
||||
manualBtn.style.color = 'var(--terminal-amber)';
|
||||
manualBtn.style.textShadow = '0 0 5px #ffb000';
|
||||
autoBtn.style.background = 'transparent';
|
||||
autoBtn.style.color = 'var(--terminal-green)';
|
||||
autoBtn.style.textShadow = 'none';
|
||||
} else {
|
||||
autoBtn.style.background = 'rgba(0,255,65,0.2)';
|
||||
autoBtn.style.color = 'var(--terminal-amber)';
|
||||
autoBtn.style.textShadow = '0 0 5px #ffb000';
|
||||
manualBtn.style.background = 'transparent';
|
||||
manualBtn.style.color = 'var(--terminal-green)';
|
||||
manualBtn.style.textShadow = 'none';
|
||||
}
|
||||
}
|
||||
renderFilteredExecutions();
|
||||
}
|
||||
|
||||
function renderFilteredExecutions() {
|
||||
const searchTerm = (document.getElementById('executionSearch')?.value || '').toLowerCase();
|
||||
const statusFilter = document.getElementById('statusFilter')?.value || '';
|
||||
|
||||
updateSubTabCounts();
|
||||
|
||||
// Filter executions
|
||||
let filtered = allExecutions.filter(e => {
|
||||
// View filter (manual vs automated)
|
||||
if (executionView === 'manual' && isAutomatedRun(e)) return false;
|
||||
if (executionView === 'automated' && !isAutomatedRun(e)) return false;
|
||||
|
||||
// Status filter
|
||||
if (statusFilter && e.status !== statusFilter) return false;
|
||||
|
||||
@@ -2580,6 +2641,7 @@
|
||||
// Initialize
|
||||
loadUser().then((success) => {
|
||||
if (success) {
|
||||
setExecutionView(executionView);
|
||||
refreshData();
|
||||
connectWebSocket();
|
||||
setInterval(refreshData, 30000);
|
||||
|
||||
Reference in New Issue
Block a user