Files
jaredandClaude Fable 5.1 eca5cb45b1
Lint / JS (eslint) (pull_request) Successful in 10s
Lint / Notify on failure (pull_request) Skipped
Lint / Deploy (pull_request) Skipped
Security / JS Security (npm audit) (pull_request) Failing after 10s
Test / JS Tests (jest) (pull_request) Successful in 14s
Lint / JS (eslint) (push) Successful in 11s
Lint / Notify on failure (push) Skipped
Lint / Deploy (push) Skipped
Security / JS Security (npm audit) (push) Failing after 11s
Test / JS Tests (jest) (push) Successful in 10s
feat(ui): TDS migration stage 3 — integration fixes, legacy removal, docs
- Fix design-system class names that do not exist in base.css (.lt-alert--error,
  .lt-form-hint); add shared .lt-modal-lg, .lt-field-error and .is-invalid rules
- Remove public/index.html and the public/base.js symlink
- package.json: start script, correct main
- README: vendored design system, Web UI section, new env vars, read-only dev mode

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HamVMDrA8RqhyxmUHgiqRp
2026-09-08 22:17:15 -04:00

356 lines
13 KiB
JavaScript

/* =====================================================================
PULSE — Workflows page (WP-D)
---------------------------------------------------------------------
Owns: DOM id prefix `wf-`, action namespace `wf:*`.
Consumes the frozen window.Pulse contract from /assets/app.js.
===================================================================== */
'use strict';
(function () {
// Example workflow definition. The Create modal is pre-filled with this
// every time it opens (fixes the old bug where the textarea was blanked).
var EXAMPLE_DEFINITION = {
steps: [
{
name: 'Example Step',
type: 'execute',
targets: ['all'],
command: "echo 'Hello from PULSE'"
}
]
};
// id -> parsed definition object, rebuilt on every list load.
var _registry = {};
// id -> raw row from the list response (name/description/etc.).
var _rowsById = {};
// workflow id currently targeted by the Run modal.
var _pendingWorkflowId = null;
function paramBadge(def) {
var params = (def && def.params) || [];
if (!params.length) return '';
var n = params.length;
return ' <span class="lt-badge lt-badge-sm">' + n + ' param' + (n > 1 ? 's' : '') + '</span>';
}
function parseDefinition(raw) {
if (raw && typeof raw === 'object') return raw;
if (typeof raw === 'string') {
try { return JSON.parse(raw); } catch (e) { return {}; }
}
return {};
}
function renderEmptyState() {
return (
'<div class="lt-empty-state">' +
'<div class="lt-empty-state-icon">&#x2699;</div>' +
'<div class="lt-empty-state-title">No workflows yet</div>' +
'<div class="lt-empty-state-body">Workflows let you run multi-step jobs across one or more workers.</div>' +
'<button type="button" class="lt-btn lt-btn-primary" data-action="wf:create-open">Create your first workflow</button>' +
'</div>'
);
}
function renderRow(w) {
var def = _registry[w.id] || {};
var isAdmin = window.Pulse.isAdmin;
var esc = window.Pulse.esc;
var actions =
'<button type="button" class="lt-btn lt-btn-sm lt-btn-primary" data-action="wf:execute" data-workflow-id="' +
esc(w.id) + '">Execute</button>';
if (isAdmin) {
actions +=
' <button type="button" class="lt-btn lt-btn-sm lt-btn-secondary" data-action="wf:edit-open" data-workflow-id="' +
esc(w.id) + '">Edit</button>' +
' <button type="button" class="lt-btn lt-btn-sm lt-btn-danger" data-action="wf:delete" data-workflow-id="' +
esc(w.id) + '" data-workflow-name="' + esc(w.name) + '">Delete</button>';
}
var createdAt = window.Pulse.fmt.dateTime(w.created_at);
return (
'<tr>' +
'<td data-label="Name">' + esc(w.name) + paramBadge(def) + '</td>' +
'<td data-label="Description">' + esc(w.description || 'No description') + '</td>' +
'<td data-label="Created">' + esc(w.created_by || 'Unknown') + ' &middot; ' + esc(createdAt) + '</td>' +
'<td data-label="Actions"><div class="lt-btn-group">' + actions + '</div></td>' +
'</tr>'
);
}
function renderList(workflows) {
var el = document.getElementById('wf-list');
if (!el) return;
if (!workflows.length) {
el.innerHTML = renderEmptyState();
return;
}
var rows = workflows.map(renderRow).join('');
el.innerHTML =
'<div class="lt-table-wrap">' +
'<table class="lt-table lt-table-responsive">' +
'<thead><tr><th>Name</th><th>Description</th><th>Created</th><th>Actions</th></tr></thead>' +
'<tbody>' + rows + '</tbody>' +
'</table>' +
'</div>';
}
function renderLoadError(message) {
var el = document.getElementById('wf-list');
if (!el) return;
el.innerHTML =
'<div class="lt-alert lt-alert--error" role="alert">' +
'<span class="lt-alert-icon">&#x26A0;</span>' +
'<span class="lt-alert-msg">' + window.Pulse.esc(message || 'Failed to load workflows') + '</span>' +
'</div>';
}
function loadWorkflows() {
return window.Pulse.api.get('/api/workflows').then(function (workflows) {
var list = workflows || [];
_registry = {};
_rowsById = {};
list.forEach(function (w) {
_rowsById[w.id] = w;
_registry[w.id] = parseDefinition(w.definition);
});
renderList(list);
}).catch(function (e) {
renderLoadError(e && e.message);
});
}
/* ---------------------------------------------------------------
Create modal
--------------------------------------------------------------- */
function showFieldError(id, message) {
var el = document.getElementById(id);
if (!el) return;
el.textContent = message || '';
el.hidden = !message;
}
function openCreateModal() {
document.getElementById('wf-create-form').reset();
document.getElementById('wf-create-definition').value = JSON.stringify(EXAMPLE_DEFINITION, null, 2);
showFieldError('wf-create-error', '');
window.lt.modal.open('wf-create-modal');
}
function submitCreateForm(formEl) {
var name = formEl.elements['name'].value.trim();
var description = formEl.elements['description'].value.trim();
var definitionText = formEl.elements['definition'].value;
var webhookUrl = formEl.elements['webhook_url'].value.trim() || null;
if (!name) {
showFieldError('wf-create-error', 'Name is required.');
return;
}
var definition;
try {
definition = JSON.parse(definitionText);
} catch (e) {
showFieldError('wf-create-error', 'Invalid JSON: ' + e.message);
return;
}
showFieldError('wf-create-error', '');
return window.Pulse.api.post('/api/workflows', {
name: name,
description: description,
definition: definition,
webhook_url: webhookUrl
}).then(function () {
window.lt.modal.close('wf-create-modal');
window.Pulse.toast.success('Workflow created');
return loadWorkflows();
}).catch(function (e) {
showFieldError('wf-create-error', (e && e.message) || 'Failed to create workflow');
});
}
/* ---------------------------------------------------------------
Edit modal (admin)
--------------------------------------------------------------- */
function openEditModal(workflowId) {
showFieldError('wf-edit-error', '');
return window.Pulse.api.get('/api/workflows/' + encodeURIComponent(workflowId)).then(function (wf) {
document.getElementById('wf-edit-id').value = wf.id;
document.getElementById('wf-edit-name').value = wf.name || '';
document.getElementById('wf-edit-description').value = wf.description || '';
document.getElementById('wf-edit-definition').value = JSON.stringify(parseDefinition(wf.definition), null, 2);
document.getElementById('wf-edit-webhook').value = wf.webhook_url || '';
window.lt.modal.open('wf-edit-modal');
}).catch(function (e) {
window.Pulse.toast.error((e && e.message) || 'Failed to load workflow');
});
}
function submitEditForm(formEl) {
var id = formEl.elements['id'].value;
var name = formEl.elements['name'].value.trim();
var description = formEl.elements['description'].value.trim();
var definitionText = formEl.elements['definition'].value;
var webhookUrl = formEl.elements['webhook_url'].value.trim() || null;
if (!name) {
showFieldError('wf-edit-error', 'Name is required.');
return;
}
var definition;
try {
definition = JSON.parse(definitionText);
} catch (e) {
showFieldError('wf-edit-error', 'Invalid JSON: ' + e.message);
return;
}
showFieldError('wf-edit-error', '');
return window.Pulse.api.put('/api/workflows/' + encodeURIComponent(id), {
name: name,
description: description,
definition: definition,
webhook_url: webhookUrl
}).then(function () {
window.lt.modal.close('wf-edit-modal');
window.Pulse.toast.success('Workflow saved');
return loadWorkflows();
}).catch(function (e) {
showFieldError('wf-edit-error', (e && e.message) || 'Failed to save workflow');
});
}
/* ---------------------------------------------------------------
Delete (admin)
--------------------------------------------------------------- */
function deleteWorkflow(workflowId, workflowName) {
return window.Pulse.confirm({
title: 'Delete Workflow',
message: 'Delete workflow "' + workflowName + '"? This cannot be undone.',
type: 'error',
confirmLabel: 'DELETE'
}).then(function (ok) {
if (!ok) return;
return window.Pulse.api.delete('/api/workflows/' + encodeURIComponent(workflowId)).then(function () {
window.Pulse.toast.success('Workflow deleted');
return loadWorkflows();
}).catch(function (e) {
window.Pulse.toast.error((e && e.message) || 'Failed to delete workflow');
});
});
}
/* ---------------------------------------------------------------
Run modal (execute): always shown, replaces the old confirm().
--------------------------------------------------------------- */
function paramFieldHtml(p) {
var esc = window.Pulse.esc;
var label = esc(p.label || p.name);
var required = p.required ? ' <span class="wf-required">*</span>' : '';
return (
'<div class="lt-form-group">' +
'<label class="lt-label" for="wf-run-param-' + esc(p.name) + '">' + label + required + '</label>' +
'<input type="text" id="wf-run-param-' + esc(p.name) + '" class="lt-input" ' +
'data-param-name="' + esc(p.name) + '" data-required="' + (p.required ? '1' : '0') + '" ' +
'placeholder="' + esc(p.placeholder || '') + '">' +
'</div>'
);
}
function openRunModal(workflowId) {
var row = _rowsById[workflowId];
var def = _registry[workflowId] || {};
var paramDefs = def.params || [];
_pendingWorkflowId = workflowId;
document.getElementById('wf-run-workflow-id').value = workflowId;
document.getElementById('wf-run-name').textContent = row ? row.name : 'this workflow';
document.getElementById('wf-run-dryrun').checked = false;
var paramsEl = document.getElementById('wf-run-params');
paramsEl.innerHTML = paramDefs.map(paramFieldHtml).join('');
window.lt.modal.open('wf-run-modal');
var first = paramsEl.querySelector('input');
if (first) setTimeout(function () { first.focus(); }, 60);
}
function submitRunForm() {
if (!_pendingWorkflowId) return;
var def = _registry[_pendingWorkflowId] || {};
var paramDefs = def.params || [];
var params = {};
for (var i = 0; i < paramDefs.length; i++) {
var p = paramDefs[i];
var el = document.getElementById('wf-run-param-' + p.name);
var val = el ? el.value.trim() : '';
if (p.required && !val) {
el.classList.add('is-invalid');
el.focus();
return;
}
el && el.classList.remove('is-invalid');
if (val) params[p.name] = val;
}
var dryRun = document.getElementById('wf-run-dryrun').checked;
var workflowId = _pendingWorkflowId;
return window.Pulse.api.post('/api/executions', {
workflow_id: workflowId,
params: params,
dry_run: dryRun
}).then(function (result) {
window.lt.modal.close('wf-run-modal');
_pendingWorkflowId = null;
window.Pulse.toast.success(dryRun ? 'Dry run started' : 'Workflow started');
window.location.href = '/executions?open=' + encodeURIComponent(result.id);
}).catch(function (e) {
window.Pulse.toast.error((e && e.message) || 'Failed to start execution');
});
}
/* ---------------------------------------------------------------
Actions + page registration
--------------------------------------------------------------- */
window.Pulse.actions.registerAll({
'wf:create-open': function () { openCreateModal(); },
'wf:create-submit': function (el) { return submitCreateForm(el); },
'wf:execute': function (el) { openRunModal(el.getAttribute('data-workflow-id')); },
'wf:edit-open': function (el) { return openEditModal(el.getAttribute('data-workflow-id')); },
'wf:edit-submit': function (el) { return submitEditForm(el); },
'wf:delete': function (el) {
return deleteWorkflow(el.getAttribute('data-workflow-id'), el.getAttribute('data-workflow-name'));
},
'wf:param-submit': function () { return submitRunForm(); }
});
// Enter in a param input submits the Run form (matches old behaviour).
document.addEventListener('keydown', function (ev) {
if (ev.key !== 'Enter') return;
var target = ev.target;
if (!target || target.tagName !== 'INPUT') return;
if (!target.closest || !target.closest('#wf-run-form')) return;
ev.preventDefault();
submitRunForm();
});
window.Pulse.registerPage({
name: 'workflows',
init: function () { return loadWorkflows(); },
refresh: function () { return loadWorkflows(); },
onEvent: function (type) {
if (type === 'workflow_created' || type === 'workflow_updated' || type === 'workflow_deleted') {
loadWorkflows();
return true;
}
return false;
}
});
})();