# PULSE - Pipelined Unified Logic & Server Engine [![Lint](https://code.lotusguild.org/LotusGuild/pulse/actions/workflows/lint.yml/badge.svg)](https://code.lotusguild.org/LotusGuild/pulse/actions?workflow=lint.yml) [![Test](https://code.lotusguild.org/LotusGuild/pulse/actions/workflows/test.yml/badge.svg)](https://code.lotusguild.org/LotusGuild/pulse/actions?workflow=test.yml) [![Security](https://code.lotusguild.org/LotusGuild/pulse/actions/workflows/security.yml/badge.svg)](https://code.lotusguild.org/LotusGuild/pulse/actions?workflow=security.yml) A distributed workflow orchestration platform for managing and executing complex multi-step operations across server clusters through a retro terminal-themed web interface. > **Security Notice:** This repository is hosted on Gitea and is version-controlled. **Never commit secrets, credentials, passwords, API keys, or any sensitive information to this repo.** All sensitive configuration belongs exclusively in `.env` files which are listed in `.gitignore` and must never be committed. This includes database passwords, worker API keys, webhook secrets, and internal IP details. **Design System**: [web_template](https://code.lotusguild.org/LotusGuild/web_template) — shared CSS, JS, and layout patterns for all LotusGuild apps ## Styling & Layout 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 ``` 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) - [`web_template/base.js`](https://code.lotusguild.org/LotusGuild/web_template/src/branch/main/base.js) — `window.lt` utilities (toast, modal, WebSocket helpers, fetch) - [`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 ![PULSE walkthrough: dashboard, workers, workflows, execution logs with an approval prompt, a multi-worker quick command and the command palette](docs/img/demo.gif) ## Why this exists I run a six-node Proxmox/Ceph cluster plus a pile of LXC containers. Day-to-day work is the same handful of questions asked of many machines: *what is the disk usage everywhere, restart that service on one node, is it safe to proceed?* SSH-ing into each box does not scale, and a full config-management system is more than I need. PULSE is a small orchestration layer: a server with a web UI, and a lightweight **worker** on each machine that executes what the server dispatches over a WebSocket. It adds what shell loops lack: multi-step **workflows** with conditions and human approval gates, scheduled commands, a full execution history, and an API that other tools call (GANDALF uses it to run network diagnostics). ```mermaid flowchart LR user["Browser
(Authelia SSO)"] -->|HTTPS| srv["PULSE server
Express + EJS + WebSocket"] gd["GANDALF"] -->|internal API| srv srv --> db[("MariaDB
workflows, executions,
schedules")] srv <-->|"WebSocket
commands / results / heartbeat"| w1["worker: node-01"] srv <-->|WebSocket| w2["worker: node-02"] srv <-->|WebSocket| w3["worker: node-03"] ``` Workflows are plain JSON with five step types: `execute`, `prompt` (pause for a human decision), `wait`, `parse` (pull `KEY=VALUE` lines from output into state) and `route` (branch on parsed state). ## Gallery **Dashboard**: recent executions at a glance and live worker status. ![Dashboard](docs/img/dashboard.png) **Workers** report system info, memory, load, uptime and task slots on every heartbeat: ![Workers](docs/img/workers.png) **Workflows** are reusable multi-step jobs. The editor takes the JSON definition directly: | Workflow list | Workflow editor | |---|---| | ![Workflows](docs/img/workflows.png) | ![Workflow editor](docs/img/workflow-editor.png) | **Executions**: full history with step-by-step logs, per-command output and durations. ![Executions](docs/img/executions.png) | Completed run with step logs | A failed command, with its error | |---|---| | ![Execution detail](docs/img/execution-detail.png) | ![Failed execution](docs/img/execution-failed.png) | **Approval gates:** a `prompt` step pauses the workflow until someone chooses an option (here, a rolling restart waits for a human to approve): ![Awaiting approval](docs/img/execution-awaiting-approval.png) **Quick Command** runs a one-off command on one or many workers at once: | Compose | Result | |---|---| | ![Quick command](docs/img/quick-command.png) | ![Quick command result](docs/img/quick-command-result.png) | **Scheduler** runs commands on an interval or hourly schedule: ![Scheduler](docs/img/scheduler.png) **Command palette** (`Ctrl+K`) and **light theme**: | Command palette | Light theme | |---|---| | ![Command palette](docs/img/command-palette.png) | ![Dashboard, light theme](docs/img/dashboard-light.png) | Screenshots are generated with Playwright against a throwaway local instance with three local workers running harmless commands (`uptime`, `df`, `echo`). See `scripts/demo/`. ## Web UI The UI is server-rendered with EJS. Every route renders `views/pages/.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/.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 `