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
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:
@@ -12,7 +12,23 @@ A distributed workflow orchestration platform for managing and executing complex
|
|||||||
|
|
||||||
## Styling & Layout
|
## 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/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)
|
- [`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/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
|
- [`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):**
|
## Web UI
|
||||||
- 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`
|
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
|
## Overview
|
||||||
|
|
||||||
@@ -311,8 +357,30 @@ DB_USER=pulse_user # Database user
|
|||||||
DB_PASSWORD=<password> # Database password
|
DB_PASSWORD=<password> # Database password
|
||||||
WORKER_API_KEY=<api-key> # Worker authentication key
|
WORKER_API_KEY=<api-key> # Worker authentication key
|
||||||
EXECUTION_RETENTION_DAYS=30 # Auto-cleanup retention (default: 30)
|
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):**
|
**Worker (.env):**
|
||||||
```bash
|
```bash
|
||||||
WORKER_NAME=pulse-worker-01 # Unique worker name
|
WORKER_NAME=pulse-worker-01 # Unique worker name
|
||||||
|
|||||||
+1
-1
@@ -84,7 +84,7 @@ function renderError(req, res, status, title, message) {
|
|||||||
<body>
|
<body>
|
||||||
<main class="lt-main lt-container">
|
<main class="lt-main lt-container">
|
||||||
<div class="lt-frame">
|
<div class="lt-frame">
|
||||||
<div class="lt-alert lt-alert-danger">
|
<div class="lt-alert lt-alert--error">
|
||||||
<strong>${esc(status)} — ${esc(title)}</strong>
|
<strong>${esc(status)} — ${esc(title)}</strong>
|
||||||
<p>${esc(message)}</p>
|
<p>${esc(message)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+2
-1
@@ -1,8 +1,9 @@
|
|||||||
{
|
{
|
||||||
"name": "pulse-server",
|
"name": "pulse-server",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "index.js",
|
"main": "server.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
"start": "node server.js",
|
||||||
"test": "jest --coverage"
|
"test": "jest --coverage"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
|
|||||||
+34
-1
@@ -131,7 +131,40 @@
|
|||||||
.pulse-scroll-x { overflow-x: auto; -webkit-overflow-scrolling: touch; }
|
.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); }
|
html[data-theme="light"] #pulse-last-refreshed { color: var(--text-muted); }
|
||||||
|
|
||||||
|
|||||||
@@ -139,12 +139,23 @@
|
|||||||
/* -------------------------------------------------------------------
|
/* -------------------------------------------------------------------
|
||||||
Data loading
|
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() {
|
async function loadWorkers() {
|
||||||
try {
|
try {
|
||||||
_workers = await Pulse.api.get('/api/workers') || [];
|
_workers = await Pulse.api.get('/api/workers') || [];
|
||||||
|
_loadFailed = false;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_workers = [];
|
_workers = [];
|
||||||
console.error('[Pulse:dashboard] failed to load workers', e);
|
reportLoadError('workers', e);
|
||||||
}
|
}
|
||||||
renderWorkers(_workers);
|
renderWorkers(_workers);
|
||||||
return _workers;
|
return _workers;
|
||||||
@@ -156,7 +167,7 @@
|
|||||||
_executions = (data && data.executions) || [];
|
_executions = (data && data.executions) || [];
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
_executions = [];
|
_executions = [];
|
||||||
console.error('[Pulse:dashboard] failed to load executions', e);
|
reportLoadError('executions', e);
|
||||||
}
|
}
|
||||||
renderExecutions(_executions);
|
renderExecutions(_executions);
|
||||||
return _executions;
|
return _executions;
|
||||||
|
|||||||
@@ -8,15 +8,10 @@
|
|||||||
===================================================================== */
|
===================================================================== */
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------
|
/* ---------------------------------------------------------------------
|
||||||
1. Modal sizing — base.css has .lt-modal-sm but no .lt-modal-lg, so the
|
1. Modal sizing — width comes from the shared `.lt-modal-lg` rule in
|
||||||
large variant is scoped to this page's two modals to avoid colliding
|
/assets/app.css; only the scroll behaviour of these two modals is
|
||||||
with any other page that defines the same class.
|
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-detail-modal .lt-modal-body,
|
||||||
#ex-compare-modal .lt-modal-body {
|
#ex-compare-modal .lt-modal-body {
|
||||||
max-height: 72vh;
|
max-height: 72vh;
|
||||||
|
|||||||
@@ -23,15 +23,4 @@
|
|||||||
color: var(--accent-red);
|
color: var(--accent-red);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* base.css defines .lt-field-error as a hook (see base.js field validation)
|
/* `.lt-field-error` and `.is-invalid` live in /assets/app.css (shared). */
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -290,11 +290,11 @@
|
|||||||
var el = document.getElementById('wf-run-param-' + p.name);
|
var el = document.getElementById('wf-run-param-' + p.name);
|
||||||
var val = el ? el.value.trim() : '';
|
var val = el ? el.value.trim() : '';
|
||||||
if (p.required && !val) {
|
if (p.required && !val) {
|
||||||
el.classList.add('wf-input-invalid');
|
el.classList.add('is-invalid');
|
||||||
el.focus();
|
el.focus();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
el && el.classList.remove('wf-input-invalid');
|
el && el.classList.remove('is-invalid');
|
||||||
if (val) params[p.name] = val;
|
if (val) params[p.name] = val;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
/root/code/web_template/base.js
|
|
||||||
-3162
File diff suppressed because it is too large
Load Diff
@@ -47,7 +47,7 @@
|
|||||||
<div class="lt-form-group">
|
<div class="lt-form-group">
|
||||||
<label class="lt-label" for="qc-command">Command</label>
|
<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>
|
<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>
|
</div>
|
||||||
|
|
||||||
<button type="submit" class="lt-btn lt-btn-primary" id="qc-execute-btn">▶ Execute Command</button>
|
<button type="submit" class="lt-btn lt-btn-primary" id="qc-execute-btn">▶ Execute Command</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user