feat(ui): TDS migration stage 2 — six page views and page modules

- Dashboard, Workers, Workflows, Executions, Quick Command, Scheduler pages on the
  shared layout, all behaviour via Pulse delegated actions (no inline handlers)
- Executions: server-driven Manual/Automated views, compare mode, detail modal with all
  log types, ?open= deep link, cross-page re-run via sessionStorage
- Workflows: run modal (dry-run available for every workflow), create modal pre-filled
  with the example definition
- Fix EJS comment in page-header partial that broke rendering

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HamVMDrA8RqhyxmUHgiqRp
This commit is contained in:
2026-09-08 21:57:33 -04:00
co-authored by Claude Fable 5.1
parent cbb91a306d
commit 68d0a906a6
16 changed files with 2845 additions and 3 deletions
+118
View File
@@ -0,0 +1,118 @@
<%#
Executions page (WP-E).
Owns DOM id prefix `ex-` and action namespace `ex:*`.
All behaviour lives in /assets/pages/executions.js (auto-loaded by the layout);
styling additions in /assets/pages/executions.css (auto-linked).
The page-header is written out inline rather than through the shared partial
because the Clear Completed button is admin-only and the partial takes its
actions as a pre-rendered string.
%>
<div class="lt-page-header">
<div>
<h1 class="lt-page-title">Executions</h1>
<p class="lt-page-subtitle">Workflow and quick-command run history</p>
</div>
<div class="lt-page-actions">
<div class="lt-btn-group">
<button type="button" class="lt-btn lt-btn-secondary lt-btn-sm" data-action="ex:refresh">&#x21bb; Refresh</button>
<button type="button" class="lt-btn lt-btn-secondary lt-btn-sm" id="ex-compare-toggle"
data-action="ex:compare-toggle" aria-pressed="false">&#x25a4; Compare Mode</button>
<button type="button" class="lt-btn lt-btn-primary lt-btn-sm is-hidden" id="ex-compare-run"
data-action="ex:compare-run">&#x2696; Compare Selected</button>
<% if (user && user.isAdmin) { %>
<button type="button" class="lt-btn lt-btn-danger lt-btn-sm" data-action="ex:clear-completed">&#x2716; Clear Completed</button>
<% } %>
</div>
</div>
</div>
<div class="lt-frame">
<span class="lt-frame-bl"></span><span class="lt-frame-br"></span>
<!-- Manual / Automated sub-tabs. base.js initTabs wires the click/keyboard
behaviour and persists lt_activeTab_/executions; the ex:view-tab action
keeps pulse_executionView (the authoritative store) in sync. -->
<div class="lt-tab-bar" role="tablist" aria-label="Execution view">
<button type="button" class="lt-tab active" role="tab" id="ex-tabbtn-manual"
aria-selected="true" aria-controls="ex-tab-manual"
data-tab="ex-tab-manual" data-action="ex:view-tab" data-view="manual">
&#x1f464; Manual Runs <span class="lt-badge lt-badge-sm" id="ex-count-manual"></span>
</button>
<button type="button" class="lt-tab" role="tab" id="ex-tabbtn-automated"
aria-selected="false" aria-controls="ex-tab-automated"
data-tab="ex-tab-automated" data-action="ex:view-tab" data-view="automated">
&#x1f916; Automated <span class="lt-badge lt-badge-sm" id="ex-count-automated"></span>
</button>
</div>
<div class="lt-alert lt-alert--warning is-hidden" id="ex-compare-hint">
<span class="lt-alert-icon">&#x25a4;</span>
<div class="lt-alert-body">
<div class="lt-alert-title">Compare mode</div>
<div class="lt-alert-msg">Select 2&ndash;5 executions to compare their outputs. Click a row to toggle selection.</div>
</div>
</div>
<div class="lt-toolbar">
<div class="lt-toolbar-left">
<div class="lt-search">
<input type="search" id="ex-search" class="lt-input lt-search-input"
placeholder="Search by command, execution ID, or workflow name…"
autocomplete="off" aria-label="Search executions"
data-input-action="ex:search">
</div>
<select id="ex-status" class="lt-select" aria-label="Status filter" data-change-action="ex:filter-status">
<option value="">All Statuses</option>
<option value="running">Running</option>
<option value="completed">Completed</option>
<option value="failed">Failed</option>
<option value="waiting">Waiting</option>
</select>
<button type="button" class="lt-btn lt-btn-ghost lt-btn-sm" data-action="ex:clear-filters">Clear Filters</button>
</div>
<div class="lt-toolbar-right">
<span id="ex-filter-stats" class="pulse-meta"></span>
</div>
</div>
<div class="lt-tab-panels">
<div class="lt-tab-panel active" id="ex-tab-manual" role="tabpanel" aria-labelledby="ex-tabbtn-manual" tabindex="-1">
<div id="ex-list-manual" class="ex-list">
<div class="lt-empty-state lt-empty-state--sm"><div class="lt-empty-state-title">Loading&hellip;</div></div>
</div>
</div>
<div class="lt-tab-panel" id="ex-tab-automated" role="tabpanel" aria-labelledby="ex-tabbtn-automated" tabindex="-1">
<div id="ex-list-automated" class="ex-list">
<div class="lt-empty-state lt-empty-state--sm"><div class="lt-empty-state-title">Loading&hellip;</div></div>
</div>
</div>
</div>
</div>
<!-- ===================== EXECUTION DETAIL MODAL ===================== -->
<div class="lt-modal-overlay" id="ex-detail-modal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="ex-detail-title">
<div class="lt-modal lt-modal-lg">
<div class="lt-modal-header">
<span class="lt-modal-title" id="ex-detail-title">Execution Details</span>
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">&#x2715;</button>
</div>
<div class="lt-modal-body" id="ex-detail-body"></div>
<div class="lt-modal-footer" id="ex-detail-footer"></div>
</div>
</div>
<!-- ===================== EXECUTION COMPARE MODAL ==================== -->
<div class="lt-modal-overlay" id="ex-compare-modal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="ex-compare-title">
<div class="lt-modal lt-modal-lg">
<div class="lt-modal-header">
<span class="lt-modal-title" id="ex-compare-title">&#x2696; Execution Comparison</span>
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">&#x2715;</button>
</div>
<div class="lt-modal-body" id="ex-compare-body"></div>
<div class="lt-modal-footer">
<button type="button" class="lt-btn lt-btn-secondary lt-btn-sm" data-modal-close>Close</button>
</div>
</div>
</div>
+89
View File
@@ -0,0 +1,89 @@
<%#
Quick Command page (WP-F). Ad-hoc command execution against one or more
workers. All data loaded client-side by /assets/pages/quick.js.
%>
<%- include('../partials/page-header', {
title: 'Quick Command',
subtitle: 'Execute a command on selected workers instantly',
actions: '<button type="button" class="lt-btn lt-btn-secondary lt-btn-sm" data-action="qc:templates-open">&#x1f4cb; Templates</button>' +
'<button type="button" class="lt-btn lt-btn-secondary lt-btn-sm" data-action="qc:history-open">&#x1f550; History</button>'
}) %>
<div class="lt-frame">
<form id="qc-form" data-submit-action="qc:execute">
<div class="lt-form-group">
<label class="lt-label">Execution Mode</label>
<div class="qc-mode-row">
<label class="qc-radio-label">
<input type="radio" class="lt-radio" name="qc-exec-mode" id="qc-mode-single" value="single" checked data-change-action="qc:mode">
<span>Single Worker</span>
</label>
<label class="qc-radio-label">
<input type="radio" class="lt-radio" name="qc-exec-mode" id="qc-mode-multi" value="multi" data-change-action="qc:mode">
<span>Multiple Workers</span>
</label>
</div>
</div>
<div class="lt-form-group" id="qc-single-mode">
<label class="lt-label" for="qc-worker">Select Worker</label>
<select class="lt-select" id="qc-worker">
<option value="">Loading workers&hellip;</option>
</select>
</div>
<div class="lt-form-group" id="qc-multi-mode" hidden>
<label class="lt-label">Select Workers</label>
<div id="qc-worker-list" class="qc-worker-list">
<div class="lt-empty-state lt-empty-state--sm"><div class="lt-empty-state-title">Loading&hellip;</div></div>
</div>
<div class="lt-btn-group qc-worker-actions">
<button type="button" class="lt-btn lt-btn-secondary lt-btn-sm" data-action="qc:select-all">Select All</button>
<button type="button" class="lt-btn lt-btn-secondary lt-btn-sm" data-action="qc:select-online">Online Only</button>
<button type="button" class="lt-btn lt-btn-secondary lt-btn-sm" data-action="qc:clear-all">Clear All</button>
</div>
</div>
<div class="lt-form-group">
<label class="lt-label" for="qc-command">Command</label>
<textarea class="lt-textarea" id="qc-command" rows="4" placeholder="Enter command to execute (e.g. 'uptime' or 'df -h')"></textarea>
<div class="lt-field-hint">Ctrl+Enter to execute</div>
</div>
<button type="submit" class="lt-btn lt-btn-primary" id="qc-execute-btn">&#x25b6; Execute Command</button>
</form>
<div id="qc-result" class="qc-result"></div>
</div>
<!-- Command Templates Modal -->
<div class="lt-modal-overlay" id="qc-templates-modal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="qc-templates-title">
<div class="lt-modal lt-modal-lg">
<div class="lt-modal-header">
<span class="lt-modal-title" id="qc-templates-title">Command Templates</span>
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">&#x2715;</button>
</div>
<div class="lt-modal-body">
<div id="qc-templates-list" class="qc-scroll-list"></div>
</div>
<div class="lt-modal-footer">
<button type="button" class="lt-btn lt-btn-ghost" data-modal-close>Close</button>
</div>
</div>
</div>
<!-- Command History Modal -->
<div class="lt-modal-overlay" id="qc-history-modal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="qc-history-title">
<div class="lt-modal lt-modal-lg">
<div class="lt-modal-header">
<span class="lt-modal-title" id="qc-history-title">Command History</span>
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">&#x2715;</button>
</div>
<div class="lt-modal-body">
<div id="qc-history-list" class="qc-scroll-list"></div>
</div>
<div class="lt-modal-footer">
<button type="button" class="lt-btn lt-btn-ghost" data-modal-close>Close</button>
</div>
</div>
</div>
+62
View File
@@ -0,0 +1,62 @@
<%#
Scheduler page (WP-F). List of scheduled commands + admin create/toggle/delete.
All data loaded client-side by /assets/pages/scheduler.js.
%>
<%- include('../partials/page-header', {
title: 'Scheduler',
subtitle: 'Automate command execution with flexible scheduling',
actions: '<button type="button" class="lt-btn lt-btn-secondary lt-btn-sm" data-action="app:refresh">&#x21bb; Refresh</button>' +
(pageConfig && pageConfig.isAdmin ? '<button type="button" class="lt-btn lt-btn-primary lt-btn-sm" data-action="sc:create-open">&#x2795; Create Schedule</button>' : '')
}) %>
<div class="lt-frame">
<div id="sc-wrap">
<div class="lt-empty-state" id="sc-loading">
<div class="lt-empty-state-title">Loading&hellip;</div>
</div>
</div>
</div>
<!-- Create Schedule Modal -->
<div class="lt-modal-overlay" id="sc-create-modal" aria-hidden="true" role="dialog" aria-modal="true" aria-labelledby="sc-create-title">
<div class="lt-modal">
<div class="lt-modal-header">
<span class="lt-modal-title" id="sc-create-title">Create Scheduled Command</span>
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">&#x2715;</button>
</div>
<form id="sc-create-form" data-submit-action="sc:create-submit">
<div class="lt-modal-body">
<div class="lt-form-group">
<label class="lt-label" for="sc-name">Schedule Name</label>
<input type="text" id="sc-name" class="lt-input" placeholder="e.g. Daily System Check">
</div>
<div class="lt-form-group">
<label class="lt-label" for="sc-command">Command</label>
<textarea id="sc-command" class="lt-textarea" rows="3" placeholder="Enter command to execute"></textarea>
</div>
<div class="lt-form-group">
<label class="lt-label">Target Workers</label>
<div id="sc-worker-list" class="qc-worker-list">
<div class="lt-empty-state lt-empty-state--sm"><div class="lt-empty-state-title">Loading&hellip;</div></div>
</div>
</div>
<div class="lt-form-group">
<label class="lt-label" for="sc-type">Schedule Type</label>
<select id="sc-type" class="lt-select" data-change-action="sc:type">
<option value="interval">Every X Minutes</option>
<option value="hourly">Every X Hours</option>
<option value="daily">Daily at Time</option>
</select>
</div>
<div class="lt-form-group" id="sc-value-container">
<label class="lt-label" for="sc-value">Interval (minutes)</label>
<input type="number" id="sc-value" class="lt-input" placeholder="e.g. 30" min="1">
</div>
</div>
<div class="lt-modal-footer">
<button type="submit" class="lt-btn lt-btn-primary">Create Schedule</button>
<button type="button" class="lt-btn lt-btn-ghost" data-modal-close>Cancel</button>
</div>
</form>
</div>
</div>
+1 -1
View File
@@ -43,7 +43,7 @@
<div class="lt-form-group">
<label class="lt-label" for="wf-create-definition">Definition (JSON)</label>
<textarea id="wf-create-definition" name="definition" class="lt-textarea wf-json-textarea" required></textarea>
<span class="lt-field-hint">Steps run in order against the selected targets.</span>
<span class="lt-form-hint">Steps run in order against the selected targets.</span>
</div>
<div class="lt-form-group">
<label class="lt-label" for="wf-create-webhook">Webhook URL (optional)</label>
+1 -2
View File
@@ -1,8 +1,7 @@
<%#
Page header partial.
Locals: title (string), subtitle (optional string), actions (optional raw HTML).
Usage from a page view:
<%- include('../partials/page-header', { title: 'Workers', subtitle: '', actions: '' }) %>
Usage from a page view: include('../partials/page-header', { title, subtitle, actions })
%>
<div class="lt-page-header">
<div>