feat(ui): TDS migration stage 3 — integration fixes, legacy removal, docs
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

- 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
This commit is contained in:
2026-09-08 22:17:15 -04:00
co-authored by Claude Fable 5.1
parent 68d0a906a6
commit eca5cb45b1
11 changed files with 129 additions and 3195 deletions
+72 -4
View File
@@ -12,7 +12,23 @@ A distributed workflow orchestration platform for managing and executing complex
## Styling & Layout
PULSE uses the **LotusGuild Terminal Design System**. For all styling, component, and layout documentation see:
PULSE uses the **LotusGuild Terminal Design System**. The design system is **vendored** into this
repo at `public/web_template/` (`base.css`, `base.js`, `VERSION`) and served from `/web_template/*`,
so the app has no runtime dependency on a sibling checkout. `public/web_template/VERSION` records
the design-system version, upstream short SHA, and sync date; asset URLs are cache-busted with it.
Update the vendored copy with:
```bash
scripts/sync-web-template.sh <path-to-web_template-checkout>
```
The script copies `base.css`/`base.js` as regular files (never symlinks) and rewrites `VERSION`.
Never hand-edit `public/web_template/` — it is excluded from ESLint and overwritten on every sync.
Pulse-local gaps in the design system live in `public/assets/app.css` (currently `.lt-modal-lg`,
`.lt-field-error`, `.is-invalid`) and are candidates for upstreaming.
Reference documentation:
- [`web_template/README.md`](https://code.lotusguild.org/LotusGuild/web_template/src/branch/main/README.md) — full component reference, CSS variables, JS API
- [`web_template/base.css`](https://code.lotusguild.org/LotusGuild/web_template/src/branch/main/base.css) — unified CSS (`.lt-*` classes)
@@ -20,9 +36,39 @@ PULSE uses the **LotusGuild Terminal Design System**. For all styling, component
- [`web_template/aesthetic_diff.md`](https://code.lotusguild.org/LotusGuild/web_template/src/branch/main/aesthetic_diff.md) — cross-app divergence analysis and convergence guide
- [`web_template/node/middleware.js`](https://code.lotusguild.org/LotusGuild/web_template/src/branch/main/node/middleware.js) — Express auth, CSRF, CSP nonce middleware
**Pending convergence items (see aesthetic_diff.md):**
- Extract inline `<style>` from `public/index.html` into `public/style.css` and extend `base.css`
- Use `lt.autoRefresh.start(refreshData, 30000)` instead of raw `setInterval`
## Web UI
The UI is server-rendered with EJS. Every route renders `views/pages/<page>.ejs` into the shared
chrome in `views/layout.ejs` (nav, header, WebSocket status dot, theme toggle, command palette).
| Route | Page | View | Page module |
|---|---|---|---|
| `/` | Dashboard | `views/pages/dashboard.ejs` | `public/assets/pages/dashboard.js` |
| `/workers` | Workers | `views/pages/workers.ejs` | `public/assets/pages/workers.js` |
| `/workflows` | Workflows | `views/pages/workflows.ejs` | `public/assets/pages/workflows.js` |
| `/executions` | Executions | `views/pages/executions.ejs` | `public/assets/pages/executions.js` |
| `/quick` | Quick Command | `views/pages/quick.ejs` | `public/assets/pages/quick.js` |
| `/scheduler` | Scheduler | `views/pages/scheduler.ejs` | `public/assets/pages/scheduler.js` |
Scripts load in a fixed order: `/web_template/base.js``/assets/app.js``/assets/pages/<page>.js`.
`app.js` owns the shell (`window.Pulse`: action registry, event bus, `Pulse.confirm`, formatters,
WebSocket, the single 30 s auto-refresh) and each page module registers itself with:
```js
Pulse.registerPage({ name, init(), refresh(), onEvent(type, data) /* return true if handled */ });
```
Page modules never attach their own listeners for UI actions — they register handlers under their
own action prefix (`dash:`, `wk:`, `wf:`, `ex:`, `qc:`, `sc:`) and the markup wires them up with
`data-action` / `data-change-action` / `data-input-action` / `data-submit-action` attributes that
`app.js` delegates. DOM ids are likewise prefixed per page. All dynamic strings go through
`Pulse.esc`, and destructive actions use the themed `Pulse.confirm` (no native `confirm()`/`alert()`).
**Content Security Policy:** pages are served under a strict nonce-based CSP (helmet), including
`script-src-attr 'none'`. There are **no inline `<script>` blocks without a nonce and no inline
event handler attributes** anywhere in `views/` or `public/assets/`; anything added there must
follow the same rule or the browser will refuse to run it. Set `PULSE_CSP_REPORT_ONLY=1` to switch
the policy to report-only (violations are reported to `/csp-report` and logged) while debugging.
## Overview
@@ -311,8 +357,30 @@ DB_USER=pulse_user # Database user
DB_PASSWORD=<password> # Database password
WORKER_API_KEY=<api-key> # Worker authentication key
EXECUTION_RETENTION_DAYS=30 # Auto-cleanup retention (default: 30)
# Web UI (all optional)
APP_NAME=PULSE # Header/boot/title app name (default: PULSE)
APP_SUBTITLE=<text> # Header subtitle (default: "Worker Orchestration // LotusGuild")
PULSE_CSP_REPORT_ONLY=1 # Serve the CSP report-only instead of enforcing it
PULSE_DEV_READONLY=1 # Local dev guard: disable all background writes (see below)
DISABLE_BACKGROUND_JOBS=1 # Alias for PULSE_DEV_READONLY
```
### Local development against a real database
Running a local instance against the production MariaDB is safe only with the read-only guard on:
```bash
PULSE_DEV_READONLY=1 PORT=8099 HOST=127.0.0.1 npm start
```
`PULSE_DEV_READONLY=1` (alias `DISABLE_BACKGROUND_JOBS=1`) disables every background writer, so a
dev instance can never mutate shared state behind your back: stale-execution recovery at startup,
the old-execution cleanup job (startup call and interval), the scheduled-command processor
(startup call and interval), and the stale-worker offline sweep. Each skip is logged at startup
alongside a `PULSE_DEV_READONLY IS ON` banner. The guard covers background jobs only — API routes
still write, so avoid destructive actions in the UI when you are pointed at production data.
**Worker (.env):**
```bash
WORKER_NAME=pulse-worker-01 # Unique worker name
+1 -1
View File
@@ -84,7 +84,7 @@ function renderError(req, res, status, title, message) {
<body>
<main class="lt-main lt-container">
<div class="lt-frame">
<div class="lt-alert lt-alert-danger">
<div class="lt-alert lt-alert--error">
<strong>${esc(status)}${esc(title)}</strong>
<p>${esc(message)}</p>
</div>
+2 -1
View File
@@ -1,8 +1,9 @@
{
"name": "pulse-server",
"version": "1.0.0",
"main": "index.js",
"main": "server.js",
"scripts": {
"start": "node server.js",
"test": "jest --coverage"
},
"keywords": [],
+34 -1
View File
@@ -131,7 +131,40 @@
.pulse-scroll-x { overflow-x: auto; -webkit-overflow-scrolling: touch; }
/* ---------------------------------------------------------------------
7. Light theme overrides for the additions above
7. Shared component gap-fillers (base.css does not ship these)
---------------------------------------------------------------------
`.lt-modal-lg` — base.css ships .lt-modal-xs/.lt-modal-sm only, but
several Pulse pages need a wide modal (JSON editors, log viewers,
side-by-side compare). Sized the same way base.css sizes its own
modifiers, and re-capped at the two breakpoints base.css caps
`.lt-modal` at, because this selector is more specific than those.
`.lt-field-error` / `.is-invalid` — base.css references a field-error
hook but ships no visual style for it.
Both are candidates for upstreaming into base.css.
--------------------------------------------------------------------- */
.lt-modal.lt-modal-lg {
width: min(900px, 95vw);
max-width: 900px;
}
@media (max-width: 767px) { .lt-modal.lt-modal-lg { max-width: 96vw; } }
@media (max-width: 479px) { .lt-modal.lt-modal-lg { max-width: 100vw; } }
.lt-field-error {
color: var(--accent-red);
font-size: 0.72rem;
letter-spacing: 0.02em;
margin-top: 4px;
}
.lt-input.is-invalid,
.lt-textarea.is-invalid,
.lt-select.is-invalid {
border-color: var(--accent-red);
}
/* ---------------------------------------------------------------------
8. Light theme overrides for the additions above
--------------------------------------------------------------------- */
html[data-theme="light"] #pulse-last-refreshed { color: var(--text-muted); }
+13 -2
View File
@@ -139,12 +139,23 @@
/* -------------------------------------------------------------------
Data loading
------------------------------------------------------------------- */
/* Toast a load failure at most once per outage, so the 30 s auto-refresh
does not stack a toast every cycle while the API is down. */
let _loadFailed = false;
function reportLoadError(what, e) {
console.error('[Pulse:dashboard] failed to load ' + what, e);
if (_loadFailed) return;
_loadFailed = true;
if (Pulse.toast) Pulse.toast.error((e && e.message) || ('Failed to load ' + what));
}
async function loadWorkers() {
try {
_workers = await Pulse.api.get('/api/workers') || [];
_loadFailed = false;
} catch (e) {
_workers = [];
console.error('[Pulse:dashboard] failed to load workers', e);
reportLoadError('workers', e);
}
renderWorkers(_workers);
return _workers;
@@ -156,7 +167,7 @@
_executions = (data && data.executions) || [];
} catch (e) {
_executions = [];
console.error('[Pulse:dashboard] failed to load executions', e);
reportLoadError('executions', e);
}
renderExecutions(_executions);
return _executions;
+3 -8
View File
@@ -8,15 +8,10 @@
===================================================================== */
/* ---------------------------------------------------------------------
1. Modal sizing — base.css has .lt-modal-sm but no .lt-modal-lg, so the
large variant is scoped to this page's two modals to avoid colliding
with any other page that defines the same class.
1. Modal sizing — width comes from the shared `.lt-modal-lg` rule in
/assets/app.css; only the scroll behaviour of these two modals is
page-specific.
--------------------------------------------------------------------- */
#ex-detail-modal .lt-modal,
#ex-compare-modal .lt-modal {
max-width: min(1100px, 94vw);
width: min(1100px, 94vw);
}
#ex-detail-modal .lt-modal-body,
#ex-compare-modal .lt-modal-body {
max-height: 72vh;
+1 -12
View File
@@ -23,15 +23,4 @@
color: var(--accent-red);
}
/* base.css defines .lt-field-error as a hook (see base.js field validation)
but ships no visual style for it — Workflows renders it directly for
JSON-parse / server-side errors, so give it one here. */
.lt-field-error {
color: var(--accent-red);
font-size: 0.72rem;
letter-spacing: 0.02em;
}
.wf-input-invalid {
border-color: var(--accent-red) !important;
}
/* `.lt-field-error` and `.is-invalid` live in /assets/app.css (shared). */
+2 -2
View File
@@ -290,11 +290,11 @@
var el = document.getElementById('wf-run-param-' + p.name);
var val = el ? el.value.trim() : '';
if (p.required && !val) {
el.classList.add('wf-input-invalid');
el.classList.add('is-invalid');
el.focus();
return;
}
el && el.classList.remove('wf-input-invalid');
el && el.classList.remove('is-invalid');
if (val) params[p.name] = val;
}
-1
View File
@@ -1 +0,0 @@
/root/code/web_template/base.js
-3162
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -47,7 +47,7 @@
<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 class="lt-form-hint">Ctrl+Enter to execute</div>
</div>
<button type="submit" class="lt-btn lt-btn-primary" id="qc-execute-btn">&#x25b6; Execute Command</button>