feat(ui): TDS migration stage 1 — EJS layout, CSP, vendored design system, shared runtime

- Vendor web_template base.css/base.js (v1.2 bbec859) under public/web_template with sync script
- Add views/layout.ejs (fixed upstream EJS comment delimiters, mobile drawer, cmd palette,
  keys help, WS status, theme toggle) and lib/render.js two-step renderer
- Add page routes for /, /workers, /workflows, /executions, /quick, /scheduler (stub views)
- helmet with strict nonce CSP (script-src-attr 'none'), /csp-report, report-only toggle
- lib/pageauth.js: shared users upsert + HTML 401/403 for page routes
- PULSE_DEV_READONLY guard disabling background writers for local testing
- public/assets/app.js shared runtime (Pulse namespace, delegated actions, WS singleton,
  themed confirm modal) and app.css
- eslint globals for browser files; ignore vendored assets

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:46:24 -04:00
co-authored by Claude Fable 5.1
parent 53b61249b0
commit cbb91a306d
24 changed files with 10750 additions and 38 deletions
+18
View File
@@ -0,0 +1,18 @@
<%#
Placeholder page view. Rendered by lib/render.js when views/pages/<view>.ejs
does not exist yet, so the shell can be exercised before the page work
packages land.
%>
<div class="lt-page-header">
<h1 class="lt-page-title"><%= pageTitle || 'Page' %></h1>
</div>
<div class="lt-frame">
<div class="lt-empty-state">
<div class="lt-empty-state-icon">&#x2699;</div>
<div class="lt-empty-state-title">Page under construction</div>
<div class="lt-empty-state-text">
This view has not been implemented yet. The shared shell, navigation and
assets are live; the page content lands with its work package.
</div>
</div>
</div>
+54
View File
@@ -0,0 +1,54 @@
<%#
Dashboard page (WP-C). Stats + recent manual executions + workers summary.
All data loaded client-side by /assets/pages/dashboard.js.
%>
<%- include('../partials/page-header', { title: 'Dashboard', subtitle: 'System overview', actions: '' }) %>
<div class="lt-stats-grid">
<div class="lt-stat-card" id="dash-stat-total" role="button" tabindex="0" data-action="dash:goto-workers" aria-label="Total workers">
<span class="lt-stat-icon lt-text-cyan" aria-hidden="true">&#x25c9;</span>
<div class="lt-stat-info">
<span class="lt-stat-value" id="dash-stat-total-val">0</span>
<span class="lt-stat-label">Workers</span>
</div>
</div>
<div class="lt-stat-card" id="dash-stat-online" role="button" tabindex="0" data-action="dash:goto-workers" aria-label="Online workers">
<span class="lt-stat-icon lt-text-green" aria-hidden="true">&#x25cf;</span>
<div class="lt-stat-info">
<span class="lt-stat-value" id="dash-stat-online-val">0</span>
<span class="lt-stat-label">Online</span>
</div>
</div>
<div class="lt-stat-card" id="dash-stat-offline" role="button" tabindex="0" data-action="dash:goto-workers" aria-label="Offline workers">
<span class="lt-stat-icon lt-text-red" aria-hidden="true">&#x25cb;</span>
<div class="lt-stat-info">
<span class="lt-stat-value" id="dash-stat-offline-val">0</span>
<span class="lt-stat-label">Offline</span>
</div>
</div>
<div class="lt-stat-card" id="dash-stat-running" role="button" tabindex="0" data-action="dash:goto-executions" aria-label="Running executions">
<span class="lt-stat-icon lt-text-amber" aria-hidden="true">&#x25b8;</span>
<div class="lt-stat-info">
<span class="lt-stat-value" id="dash-stat-running-val">0</span>
<span class="lt-stat-label">Running</span>
</div>
</div>
</div>
<div class="lt-frame">
<div class="lt-section-header">Recent Executions</div>
<div id="dash-executions-wrap">
<div class="lt-empty-state lt-empty-state--sm" id="dash-executions-loading">
<div class="lt-empty-state-title">Loading&hellip;</div>
</div>
</div>
</div>
<div class="lt-frame">
<div class="lt-section-header">Workers</div>
<div id="dash-workers-wrap">
<div class="lt-empty-state lt-empty-state--sm" id="dash-workers-loading">
<div class="lt-empty-state-title">Loading&hellip;</div>
</div>
</div>
</div>
+14
View File
@@ -0,0 +1,14 @@
<%#
Workers page (WP-C). Grid of worker cards, patched in place on worker_update.
%>
<%- include('../partials/page-header', {
title: 'Workers',
subtitle: 'Connected worker agents',
actions: '<button type="button" class="lt-btn lt-btn-secondary lt-btn-sm" data-action="app:refresh">&#x21bb; Refresh</button>'
}) %>
<div id="wk-grid" class="wk-grid">
<div class="lt-empty-state" id="wk-loading">
<div class="lt-empty-state-title">Loading&hellip;</div>
</div>
</div>
+125
View File
@@ -0,0 +1,125 @@
<%#
Workflows page (WP-D).
Locals: user, nonce, pageTitle, activeNav, assetVersion, pageConfig.
All data comes from /api/workflows*; pageConfig only carries the example
definition JSON used to pre-fill the Create modal.
%>
<%- include('../partials/page-header', {
title: 'Workflows',
subtitle: 'Reusable multi-step jobs run against one or more workers.',
actions:
'<button type="button" class="lt-btn lt-btn-secondary" data-action="app:refresh">Refresh</button>' +
'<button type="button" class="lt-btn lt-btn-primary" data-action="wf:create-open">+ Create Workflow</button>'
}) %>
<div class="lt-frame">
<div class="lt-section-header">Workflows</div>
<div class="lt-section-body" id="wf-list">
<div class="lt-empty-state">
<div class="lt-empty-state-icon">&#x2699;</div>
<div class="lt-empty-state-title">Loading workflows&hellip;</div>
</div>
</div>
</div>
<!-- Create Workflow Modal -->
<div class="lt-modal-overlay" id="wf-create-modal" aria-hidden="true" role="dialog"
aria-modal="true" aria-labelledby="wf-create-modal-title">
<div class="lt-modal lt-modal-lg">
<div class="lt-modal-header">
<span class="lt-modal-title" id="wf-create-modal-title">Create Workflow</span>
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">&#x2715;</button>
</div>
<form id="wf-create-form" data-submit-action="wf:create-submit">
<div class="lt-modal-body">
<div class="lt-form-group">
<label class="lt-label" for="wf-create-name">Name</label>
<input type="text" id="wf-create-name" name="name" class="lt-input" required placeholder="Workflow Name" autocomplete="off">
</div>
<div class="lt-form-group">
<label class="lt-label" for="wf-create-description">Description</label>
<textarea id="wf-create-description" name="description" class="lt-textarea" placeholder="Description"></textarea>
</div>
<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>
</div>
<div class="lt-form-group">
<label class="lt-label" for="wf-create-webhook">Webhook URL (optional)</label>
<input type="url" id="wf-create-webhook" name="webhook_url" class="lt-input" placeholder="https://example.com/webhook">
</div>
<div class="lt-field-error" id="wf-create-error" hidden></div>
</div>
<div class="lt-modal-footer">
<button type="submit" class="lt-btn lt-btn-primary">Create</button>
<button type="button" class="lt-btn lt-btn-ghost" data-modal-close>Cancel</button>
</div>
</form>
</div>
</div>
<!-- Edit Workflow Modal (admin) -->
<div class="lt-modal-overlay" id="wf-edit-modal" aria-hidden="true" role="dialog"
aria-modal="true" aria-labelledby="wf-edit-modal-title">
<div class="lt-modal lt-modal-lg">
<div class="lt-modal-header">
<span class="lt-modal-title" id="wf-edit-modal-title">Edit Workflow</span>
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">&#x2715;</button>
</div>
<form id="wf-edit-form" data-submit-action="wf:edit-submit">
<input type="hidden" id="wf-edit-id" name="id">
<div class="lt-modal-body">
<div class="lt-form-group">
<label class="lt-label" for="wf-edit-name">Name</label>
<input type="text" id="wf-edit-name" name="name" class="lt-input" required placeholder="Workflow Name" autocomplete="off">
</div>
<div class="lt-form-group">
<label class="lt-label" for="wf-edit-description">Description</label>
<textarea id="wf-edit-description" name="description" class="lt-textarea" placeholder="Description"></textarea>
</div>
<div class="lt-form-group">
<label class="lt-label" for="wf-edit-definition">Definition (JSON)</label>
<textarea id="wf-edit-definition" name="definition" class="lt-textarea wf-json-textarea wf-json-textarea-lg" required></textarea>
</div>
<div class="lt-form-group">
<label class="lt-label" for="wf-edit-webhook">Webhook URL (optional)</label>
<input type="url" id="wf-edit-webhook" name="webhook_url" class="lt-input" placeholder="https://example.com/webhook">
</div>
<div class="lt-field-error" id="wf-edit-error" hidden></div>
</div>
<div class="lt-modal-footer">
<button type="submit" class="lt-btn lt-btn-primary">Save</button>
<button type="button" class="lt-btn lt-btn-ghost" data-modal-close>Cancel</button>
</div>
</form>
</div>
</div>
<!-- Run Workflow Modal (execute, with optional params + dry run) -->
<div class="lt-modal-overlay" id="wf-run-modal" aria-hidden="true" role="dialog"
aria-modal="true" aria-labelledby="wf-run-modal-title">
<div class="lt-modal lt-modal-sm">
<div class="lt-modal-header">
<span class="lt-modal-title" id="wf-run-modal-title">Run Workflow</span>
<button type="button" class="lt-modal-close" data-modal-close aria-label="Close">&#x2715;</button>
</div>
<form id="wf-run-form" data-submit-action="wf:param-submit">
<input type="hidden" id="wf-run-workflow-id" name="workflow_id">
<div class="lt-modal-body">
<p class="lt-text-sm" id="wf-run-name"></p>
<div id="wf-run-params"></div>
<div class="lt-form-group">
<label class="wf-checkbox-label">
<input type="checkbox" id="wf-run-dryrun" class="lt-checkbox">
Dry Run (simulate, no commands executed)
</label>
</div>
</div>
<div class="lt-modal-footer">
<button type="submit" class="lt-btn lt-btn-primary">&#x25B6; Run</button>
<button type="button" class="lt-btn lt-btn-ghost" data-modal-close>Cancel</button>
</div>
</form>
</div>
</div>