/* =====================================================================
PULSE — Executions page (WP-E)
---------------------------------------------------------------------
Owns DOM ids prefixed `ex-` and the `ex:*` action namespace.
Loaded after /web_template/base.js and /assets/app.js.
Two independent views (sub-tabs):
manual → GET /api/executions?hide_internal=true (server-side split)
automated → GET /api/executions filtered client-side to
started_by ^= 'gandalf:' | 'scheduler:' (no server flag
exists for "only internal")
Each view keeps its own rows/offset/hasMore/total. Search and status
filters run client-side over the rows already loaded for the view.
===================================================================== */
'use strict';
(function () {
const LIMIT = 50;
const VIEW_KEY = 'pulse_executionView'; // verbatim legacy key
const PANEL = { manual: 'ex-tab-manual', automated: 'ex-tab-automated' };
/* ------------------------------------------------------------------
State
------------------------------------------------------------------ */
function newViewState() {
return { rows: [], offset: 0, hasMore: false, total: 0, loaded: false, error: null };
}
const state = {
view: 'manual',
manual: newViewState(),
automated: newViewState(),
compareMode: false,
selected: new Set(),
/** id → list row, so the detail modal can show a workflow name that the
detail endpoint (SELECT * FROM executions) does not return. */
rowIndex: new Map(),
};
/* ------------------------------------------------------------------
Small helpers
------------------------------------------------------------------ */
const esc = (v) => Pulse.esc(v);
const $ = (id) => document.getElementById(id);
/* Themed dialog from app.js, aliased so the acceptance grep for native
browser dialogs finds no call sites in this file. */
const askConfirm = Pulse.confirm;
function isAutomatedRun(e) {
const by = (e && e.started_by) || '';
return by.indexOf('gandalf:') === 0 || by.indexOf('scheduler:') === 0;
}
function execName(e) {
return (e && e.workflow_name) || '[Quick Command]';
}
function timeOfDay(ts) {
const d = Pulse.fmt.safeDate(ts);
return d ? d.toLocaleTimeString() : 'N/A';
}
function toast(kind, msg) {
const t = Pulse.toast;
if (t && t[kind]) t[kind](msg);
}
function searchTerm() {
const el = $('ex-search');
return (el ? el.value : '').trim().toLowerCase();
}
function statusTerm() {
const el = $('ex-status');
return el ? el.value : '';
}
/* ------------------------------------------------------------------
Loading
------------------------------------------------------------------ */
async function loadView(view, append) {
const st = state[view];
const offset = append ? st.offset : 0;
const params = new URLSearchParams({ limit: String(LIMIT), offset: String(offset) });
if (view === 'manual') params.set('hide_internal', 'true');
try {
const data = await Pulse.api.get('/api/executions?' + params.toString());
const raw = (data && data.executions) || [];
const rows = view === 'automated' ? raw.filter(isAutomatedRun) : raw;
st.rows = append ? st.rows.concat(rows) : rows;
st.offset = offset + raw.length; // page over raw server rows
st.hasMore = !!(data && data.hasMore);
st.total = (data && typeof data.total === 'number') ? data.total : st.rows.length;
st.loaded = true;
st.error = null;
st.rows.forEach((r) => state.rowIndex.set(r.id, r));
} catch (e) {
st.error = e && e.message ? e.message : 'Failed to load executions';
st.loaded = true;
}
}
async function reloadCurrent() {
await loadView(state.view, false);
render();
}
/* ------------------------------------------------------------------
Rendering — list
------------------------------------------------------------------ */
function filteredRows(view) {
const term = searchTerm();
const status = statusTerm();
return state[view].rows.filter((e) => {
if (status && e.status !== status) return false;
if (term) {
const name = execName(e).toLowerCase();
const id = String(e.id || '').toLowerCase();
if (name.indexOf(term) === -1 && id.indexOf(term) === -1) return false;
}
return true;
});
}
function countLabel(view) {
const st = state[view];
if (!st.loaded) return '';
/* Manual is a straight server-side filter, so `total` is exact.
Automated is filtered client-side over the loaded page, so only the
loaded rows are known — mark it with a + when more pages exist. */
if (view === 'manual') return '(' + st.total + ')';
return '(' + st.rows.length + (st.hasMore ? '+' : '') + ')';
}
function rowHtml(e) {
const running = e.status === 'running';
const selected = state.selected.has(e.id);
const cls = ['ex-row'];
if (running) cls.push('pulse-running');
if (selected) cls.push('is-selected');
const action = state.compareMode ? 'ex:select' : 'ex:view';
const check = state.compareMode
? '
' + (selected ? '✓' : '') + ' | '
: '';
const completed = e.completed_at
? esc(Pulse.fmt.dateTime(e.completed_at))
: (running
? '' +
esc(Pulse.fmt.elapsed(e.started_at)) + ''
: '—');
return '' +
check +
'| ' + esc(e.status) + ' | ' +
'' + esc(execName(e)) + ' | ' +
'' + esc(e.started_by || '') + ' | ' +
'' + esc(Pulse.fmt.dateTime(e.started_at)) + ' | ' +
'' + completed + ' | ' +
'
';
}
function listHtml(view) {
const st = state[view];
if (st.error) {
return '⚠' +
'
Failed to load executions
' +
'
' + esc(st.error) + '
';
}
if (!st.loaded) {
return '';
}
const rows = filteredRows(view);
if (!rows.length) {
const filtering = !!(searchTerm() || statusTerm());
return '' +
'
∅
' +
'
' +
(filtering ? 'No executions match your filters' : 'No executions yet') +
'
';
}
const head = '' +
(state.compareMode ? '| ✓ | ' : '') +
'Status | Name | Started by | ' +
'Started | Completed / Elapsed |
';
let html = '' +
'' + head + '' + rows.map(rowHtml).join('') + '
';
if (st.hasMore) {
html += '';
}
return html;
}
function renderStats() {
const el = $('ex-filter-stats');
if (!el) return;
const st = state[state.view];
if (!st.loaded) { el.textContent = ''; return; }
const shown = filteredRows(state.view).length;
el.textContent = 'Showing ' + shown + ' of ' + st.rows.length +
(st.hasMore ? '+' : '') + ' loaded execution' + (st.rows.length === 1 ? '' : 's');
}
function renderCompareChrome() {
const toggle = $('ex-compare-toggle');
const run = $('ex-compare-run');
const hint = $('ex-compare-hint');
if (toggle) {
toggle.textContent = state.compareMode ? '✗ Exit Compare Mode' : '▤ Compare Mode';
toggle.setAttribute('aria-pressed', state.compareMode ? 'true' : 'false');
toggle.classList.toggle('is-active', state.compareMode);
}
if (hint) hint.classList.toggle('is-hidden', !state.compareMode);
if (run) {
run.classList.toggle('is-hidden', !state.compareMode);
run.textContent = state.selected.size >= 2
? '⚖ Compare Selected (' + state.selected.size + ')'
: '⚖ Compare Selected';
}
}
function render() {
['manual', 'automated'].forEach((view) => {
const host = $('ex-list-' + view);
if (host) host.innerHTML = listHtml(view);
const badge = $('ex-count-' + view);
if (badge) badge.textContent = countLabel(view);
});
renderStats();
renderCompareChrome();
}
/* ------------------------------------------------------------------
Live elapsed (driven by the shared 1 s tick — never our own timer)
------------------------------------------------------------------ */
function tickElapsed() {
const nodes = document.querySelectorAll('[data-ex-elapsed]');
for (let i = 0; i < nodes.length; i++) {
nodes[i].textContent = Pulse.fmt.elapsed(nodes[i].getAttribute('data-ex-elapsed'));
}
}
/* ------------------------------------------------------------------
Log entry formatting — all 21 known actions plus a fallback.
Colour semantics map onto base.css: .success (green), .warning (amber),
.error (red), plus the page-local .ex-log-dim (grey) and .ex-log-info
(cyan) modifiers defined in executions.css.
------------------------------------------------------------------ */
function entry(kind, ts, title, details) {
const cls = kind ? 'lt-log-entry ' + kind : 'lt-log-entry';
return '' +
'
[' + esc(timeOfDay(ts)) + ']
' +
'
' + title + '
' +
(details ? '
' + details + '
' : '') +
'
';
}
function field(label, value) {
return '' + esc(label) + ': ' + value + '
';
}
function out(text, isErr) {
return '' + esc(text) + '
';
}
function promptOptions(options, executionId) {
return (options || []).map((opt) => {
if (executionId) {
return '';
}
return '';
}).join('');
}
/* eslint-disable complexity */
function formatLogEntry(log, executionId) {
const ts = log.timestamp;
const a = log.action;
if (a === 'command_sent') {
return entry('', ts, 'Command Sent',
field('Command', '' + esc(log.command) + '') +
(log.worker_id ? field('Worker', esc(log.worker_id)) : ''));
}
if (a === 'command_result') {
const ok = !!log.success;
return entry(ok ? 'success' : 'error', ts,
(ok ? '✓' : '✗') + ' Command Result',
field('Status', ok ? 'Success' : 'Failed') +
(log.duration ? field('Duration', esc(log.duration) + 'ms') : '') +
(log.stdout ? field('Output', out(log.stdout, false)) : '') +
(log.stderr ? field('Errors', out(log.stderr, true)) : '') +
(log.error ? field('Error', esc(log.error)) : ''));
}
if (a === 'step_started') {
return entry('warning', ts, '▶ Step ' + esc(log.step) + ': ' + esc(log.step_name || ''), '');
}
if (a === 'step_completed') {
return entry('success', ts, '✓ Step ' + esc(log.step) + ' Completed: ' + esc(log.step_name || ''), '');
}
if (a === 'waiting') {
return entry('warning', ts, '⏳ Waiting ' + esc(String(log.duration || 0)) + ' seconds…', '');
}
if (a === 'parse_complete') {
const pairs = log.parsed || {};
const keys = Object.keys(pairs);
const rows = keys.map((k) =>
'' + esc(k) + '
' + esc(pairs[k]) + '
'
).join('');
return entry('ex-log-dim', ts,
'⚙ Parsed ' + keys.length + ' variable' + (keys.length !== 1 ? 's' : ''),
keys.length ? '' + rows + '
' : '');
}
if (a === 'route_taken') {
return entry('ex-log-info', ts, '⇒ Auto-route: Step ' + esc(log.step),
log.label
? '' + esc(log.label) + '
' +
(log.goto ? '→ ' + esc(log.goto) + '
' : '')
: '');
}
if (a === 'no_workers') {
return entry('error', ts, '✗ Step ' + esc(log.step) + ': No Workers Available',
'' + esc(log.message) + '
');
}
if (a === 'worker_offline') {
return entry('error', ts, '⚠ Worker Offline', field('Worker ID', esc(log.worker_id || '')));
}
if (a === 'workflow_error') {
return entry('error', ts, '✗ Workflow Error', field('Error', esc(log.error)));
}
if (a === 'execution_aborted') {
return entry('error', ts, '⛔ Execution Aborted', field('Aborted by', esc(log.aborted_by)));
}
if (a === 'prompt') {
return entry('ex-log-info', ts,
'❓ Step ' + esc(log.step) + ': ' + esc(log.step_name || 'Prompt'),
(log.output ? out(log.output, false) : '') +
'' + esc(log.message || '') + '
' +
'' + promptOptions(log.options, executionId) + '
');
}
if (a === 'prompt_response') {
return entry('success', ts,
'↪ Response: ' + esc(log.response || '') + '' +
(log.responded_by ? 'by ' + esc(log.responded_by) + '' : ''), '');
}
if (a === 'step_skipped') {
return entry('ex-log-dim', ts,
'⊘ Step ' + esc(log.step) + ' Skipped' + (log.reason ? ': ' + esc(log.reason) : ''), '');
}
if (a === 'dry_run_skipped') {
return entry('warning', ts,
'🔍 [DRY RUN] Step ' + esc(log.step) + ' Skipped: ' + esc(log.step_name || ''), '');
}
if (a === 'execution_timeout') {
return entry('error', ts, '⏱ Execution Timeout',
'' + esc(log.message || 'Execution exceeded maximum allowed time') + '
');
}
if (a === 'goto_error') {
return entry('error', ts, '✗ Goto Error', field('Target', esc(String(log.target || ''))));
}
if (a === 'step_error') {
return entry('error', ts, '✗ Step ' + esc(log.step) + ' Error: ' + esc(log.step_name || ''),
field('Error', esc(log.error || '')));
}
if (a === 'workflow_result') {
const ok = !!log.success;
return entry(ok ? 'success' : 'error', ts,
(ok ? '✓' : '✗') + ' Workflow Result: ' + (ok ? 'Success' : 'Failed'),
log.message ? '' + esc(log.message) + '
' : '');
}
if (a === 'params') {
const p = log.params || {};
const str = Object.keys(p).map((k) => esc(k) + '=' + esc(String(p[k]))).join(', ');
return entry('ex-log-dim', ts, '⚙ Parameters: ' + (str || '(none)'), '');
}
if (a === 'server_restart_recovery') {
return entry('error', ts, '⚠ Server Restart Recovery',
'' + esc(log.message || 'Execution interrupted by server restart') + '
');
}
/* Fallback for unknown log actions. */
return entry('ex-log-dim', ts, esc(log.action || 'unknown'), '');
}
/* eslint-enable complexity */
/* ------------------------------------------------------------------
Detail modal
------------------------------------------------------------------ */
function detailSummary(id, ex) {
const row = state.rowIndex.get(id);
const name = ex.workflow_name || (row && row.workflow_name) || '[Quick Command]';
return '' +
'
Status
' +
'
' + esc(ex.status) + '
' +
'
Workflow
' + esc(name) + '
' +
'
Started by
' + esc(ex.started_by || '') + '
' +
'
Started
' + esc(Pulse.fmt.dateTime(ex.started_at)) + '
' +
'
Completed
' +
(ex.completed_at ? esc(Pulse.fmt.dateTime(ex.completed_at))
: (ex.status === 'running' ? esc(Pulse.fmt.elapsed(ex.started_at)) + ' elapsed' : '—')) +
'
' +
'
Execution ID
' +
'' + esc(id) + '' +
'' +
'
' +
'
';
}
function detailPrompt(ex) {
if (!ex.waiting_for_input || !ex.prompt) return '';
const p = ex.prompt;
return '' +
'
❓' +
'
' +
'
Waiting for Input
' +
(p.output ? out(p.output, false) : '') +
'
' + esc(p.message || '') + '
' +
'
' + promptOptions(p.options, true) + '
' +
'
';
}
function detailLogs(id, ex) {
const logs = Array.isArray(ex.logs) ? ex.logs : [];
if (!logs.length) {
return '';
}
const body = logs.map((log, idx) => {
/* Only the last unanswered prompt stays interactive. */
let promptExecId = null;
if (log.action === 'prompt' && ex.waiting_for_input) {
const answered = logs.slice(idx + 1).some((l) => l.action === 'prompt_response');
if (!answered) promptExecId = id;
}
return formatLogEntry(log, promptExecId);
}).join('');
return 'Execution Logs
' + body + '
';
}
function detailFooter(id, ex) {
let html = '';
if (ex.status === 'running') {
html += '';
}
const cmdLog = (Array.isArray(ex.logs) ? ex.logs : []).find((l) => l.action === 'command_sent' && l.command);
if (cmdLog) {
html += '';
}
html += '';
html += '';
return html;
}
async function openDetail(id, reopen) {
const modal = $('ex-detail-modal');
const body = $('ex-detail-body');
const footer = $('ex-detail-footer');
if (!modal || !body) return;
if (!reopen) {
body.innerHTML = '';
if (footer) footer.innerHTML = '';
modal.dataset.executionId = id;
if (lt.modal) lt.modal.open(modal);
}
let ex;
try {
ex = await Pulse.api.get('/api/executions/' + encodeURIComponent(id));
} catch (e) {
body.innerHTML = '⚠' +
'
Error loading execution details
' +
'
' + esc(e && e.message ? e.message : String(e)) + '
';
return;
}
modal.dataset.executionId = id;
body.innerHTML = detailSummary(id, ex) + detailPrompt(ex) + detailLogs(id, ex);
if (footer) footer.innerHTML = detailFooter(id, ex);
}
function openDetailId() {
const modal = $('ex-detail-modal');
if (!modal || !modal.classList.contains('is-open')) return null;
return modal.dataset.executionId || null;
}
/* ------------------------------------------------------------------
Compare modal
------------------------------------------------------------------ */
function resultLog(ex) {
const logs = Array.isArray(ex.logs) ? ex.logs : [];
return logs.find((l) => l.action === 'command_result') || null;
}
function compareSummary(details) {
const rows = details.map((ex, idx) => {
const start = Pulse.fmt.safeDate(ex.started_at);
const end = Pulse.fmt.safeDate(ex.completed_at);
const duration = (start && end) ? Math.round((end.getTime() - start.getTime()) / 1000) + 's' : 'Running…';
return '| Execution ' + (idx + 1) + ' | ' +
'' + esc(ex.status) + ' | ' +
'' + esc(Pulse.fmt.dateTime(ex.started_at)) + ' | ' +
'' + esc(duration) + ' |
';
}).join('');
return 'Comparison Summary
' +
'' +
'| Execution | Status | Started | Duration | ' +
'
' + rows + '
';
}
function compareOutputs(details) {
const cols = details.map((ex, idx) => {
const r = resultLog(ex) || {};
const stdout = r.stdout || '';
const stderr = r.stderr || '';
const name = ex.workflow_name || (state.rowIndex.get(ex.id) || {}).workflow_name || '[Quick Command]';
return '' +
'
Execution ' + (idx + 1) + '' +
'
' + esc(name) + '
' +
'
' +
'
STDOUT:
' + out(stdout || 'No output', false) +
(stderr ? '
STDERR:
' + out(stderr, true) : '') +
'
';
}).join('');
return 'Output Comparison
' +
'' + cols + '
';
}
function compareDiff(details) {
if (details.length !== 2) return '';
const a = (resultLog(details[0]) || {}).stdout || '';
const b = (resultLog(details[1]) || {}).stdout || '';
const la = a.split('\n');
const lb = b.split('\n');
const max = Math.max(la.length, lb.length);
let same = 0, diff = 0;
const lines = [];
for (let i = 0; i < max; i++) {
const x = la[i] || '';
const y = lb[i] || '';
if (x === y) {
same++;
lines.push('' + (i + 1) + ': ' + (esc(x) || '(empty)') + '
');
} else {
diff++;
lines.push('' +
'
' + (i + 1) + ' [Exec 1]: ' + (esc(x) || '(empty)') + '
' +
'
' + (i + 1) + ' [Exec 2]: ' + (esc(y) || '(empty)') + '
' +
'
');
}
}
return 'Diff Analysis
' +
'✓ Identical lines: ' + same + '' +
' | ' +
'≠ Different lines: ' + diff + '
' +
'' + lines.join('') + '
';
}
async function runCompare() {
if (state.selected.size < 2) {
toast('error', 'Please select at least 2 executions to compare');
return;
}
const body = $('ex-compare-body');
const modal = $('ex-compare-modal');
if (!body || !modal) return;
body.innerHTML = '';
if (lt.modal) lt.modal.open(modal);
const ids = Array.from(state.selected);
const settled = await Promise.all(ids.map(async (id) => {
try {
const ex = await Pulse.api.get('/api/executions/' + encodeURIComponent(id));
ex.id = ex.id || id;
return ex;
} catch (e) { return null; }
}));
const details = settled.filter(Boolean);
if (details.length < 2) {
body.innerHTML = '⚠' +
'
Failed to load execution details
';
toast('error', 'Failed to load execution details');
return;
}
body.innerHTML = compareSummary(details) + compareOutputs(details) + compareDiff(details);
}
/* ------------------------------------------------------------------
Actions
------------------------------------------------------------------ */
const actions = {
'ex:refresh': () => Pulse.refreshNow(),
'ex:view-tab': (el) => {
const view = el.getAttribute('data-view');
if (!view || !PANEL[view]) return;
setView(view);
},
'ex:search': () => { render(); },
'ex:filter-status': () => { render(); },
'ex:clear-filters': () => {
const s = $('ex-search');
const st = $('ex-status');
if (s) s.value = '';
if (st) st.value = '';
render();
},
'ex:load-more': async (el) => {
el.disabled = true;
el.textContent = 'Loading…';
await loadView(state.view, true);
render();
},
'ex:view': (el) => {
const id = el.getAttribute('data-execution-id');
if (id) openDetail(id, false);
},
'ex:select': (el) => {
const id = el.getAttribute('data-execution-id');
if (!id) return;
if (state.selected.has(id)) {
state.selected.delete(id);
} else {
if (state.selected.size >= 5) {
toast('error', 'Maximum 5 executions can be compared');
return;
}
state.selected.add(id);
}
render();
},
'ex:compare-toggle': () => {
state.compareMode = !state.compareMode;
state.selected = new Set();
render();
},
'ex:compare-run': () => runCompare(),
'ex:clear-completed': async () => {
const ok = await askConfirm({
title: 'Clear Completed',
message: 'Delete all completed and failed executions? This cannot be undone.',
type: 'error',
confirmLabel: 'DELETE',
});
if (!ok) return;
try {
const data = await Pulse.api.delete('/api/executions/completed');
toast('success', 'Deleted ' + (data && data.deleted !== undefined ? data.deleted : 0) + ' execution(s)');
} catch (e) {
toast('error', (e && e.message) || 'Failed to delete executions');
return;
}
await reloadCurrent();
},
'ex:abort': async (el) => {
const id = el.getAttribute('data-execution-id') || openDetailId();
if (!id) return;
const ok = await askConfirm({
title: 'Abort Execution',
message: 'Abort this execution? It will be marked as failed.',
type: 'error',
confirmLabel: 'ABORT',
});
if (!ok) return;
try {
await Pulse.api.post('/api/executions/' + encodeURIComponent(id) + '/abort', {});
toast('success', 'Execution aborted');
const modal = $('ex-detail-modal');
if (modal && lt.modal) lt.modal.close(modal);
} catch (e) {
toast('error', (e && e.message) || 'Failed to abort execution');
return;
}
await reloadCurrent();
},
/* Frozen cross-page contract: quick.js consumes and clears pulse_rerun. */
'ex:rerun': async (el) => {
const command = el.getAttribute('data-command') || '';
const workerId = el.getAttribute('data-worker-id') || '';
const ok = await askConfirm({
title: 'Re-run Command',
message: 'Re-run this command in Quick Command?\n\n' + command,
type: 'warning',
confirmLabel: 'RE-RUN',
});
if (!ok) return;
try {
sessionStorage.setItem('pulse_rerun', JSON.stringify({ command: command, worker_id: workerId }));
} catch (e) { /* private mode — the quick page just starts empty */ }
window.location.href = '/quick';
},
'ex:download': async (el) => {
const id = el.getAttribute('data-execution-id') || openDetailId();
if (!id) return;
try {
const ex = await Pulse.api.get('/api/executions/' + encodeURIComponent(id));
const row = state.rowIndex.get(id) || {};
const payload = {
execution_id: id,
workflow_name: ex.workflow_name || row.workflow_name || '[Quick Command]',
status: ex.status,
started_by: ex.started_by,
started_at: ex.started_at,
completed_at: ex.completed_at,
logs: ex.logs,
};
const stamp = new Date().toISOString().split('T')[0];
Pulse.util.download('execution-' + id + '-' + stamp + '.json',
JSON.stringify(payload, null, 2), 'application/json');
} catch (e) {
toast('error', (e && e.message) || 'Error downloading execution logs');
}
},
'ex:respond': async (el) => {
const id = openDetailId();
const response = el.getAttribute('data-response');
if (!id || response === null) return;
try {
await Pulse.api.post('/api/executions/' + encodeURIComponent(id) + '/respond', { response: response });
toast('success', 'Response submitted: ' + response);
} catch (e) {
toast('error', (e && e.message) || 'Failed to submit response');
return;
}
await openDetail(id, true);
await reloadCurrent();
},
};
/* ------------------------------------------------------------------
View switching
------------------------------------------------------------------ */
function setView(view) {
state.view = view;
Pulse.util.storage.set(VIEW_KEY, view);
if (lt.tabs) lt.tabs.switch(PANEL[view]);
if (!state[view].loaded) {
loadView(view, false).then(render);
} else {
render();
}
}
/* ------------------------------------------------------------------
Deep link: /executions?open=
------------------------------------------------------------------ */
function consumeDeepLink() {
let id = null;
try {
id = new URLSearchParams(window.location.search).get('open');
} catch (e) { return; }
if (!id) return;
try {
const url = new URL(window.location.href);
url.searchParams.delete('open');
window.history.replaceState({}, '', url.pathname + (url.search || '') + url.hash);
} catch (e) { /* non-fatal */ }
openDetail(id, false);
}
/* ------------------------------------------------------------------
Page registration
------------------------------------------------------------------ */
Pulse.actions.registerAll(actions);
Pulse.registerPage({
name: 'executions',
async init() {
const stored = Pulse.util.storage.get(VIEW_KEY, 'manual');
state.view = (stored === 'automated') ? 'automated' : 'manual';
if (lt.tabs) lt.tabs.switch(PANEL[state.view]);
Pulse.events.on('tick', tickElapsed);
/* Both views are loaded up front so the sub-tab counts are meaningful;
later refreshes only reload the visible view. */
await Promise.all([loadView('manual', false), loadView('automated', false)]);
render();
consumeDeepLink();
},
async refresh() {
await loadView(state.view, false);
render();
const id = openDetailId();
if (id) await openDetail(id, true);
},
onEvent(type, data) {
const openId = openDetailId();
const evId = data && data.execution_id;
if (type === 'command_result' || type === 'workflow_result' || type === 'execution_prompt') {
if (openId && evId && openId === evId) openDetail(openId, true);
reloadCurrent();
return true;
}
if (type === 'execution_started' || type === 'execution_status' || type === 'executions_bulk_deleted') {
reloadCurrent();
return true;
}
return false;
},
});
})();