chore: trim distribution clutter and add lint/test tooling

Keep only example workflows in default/, move site-specific YAMLs to gitignored local/, remove the duplicate get-current-time plugin install, document personal plugins, clarify PM2 ecosystem usage, add shared Vitest and web ESLint, and extract OpsStatusCard for ops cards.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-22 06:30:37 +07:00
co-authored by Cursor
parent db884808f2
commit f9f21052d9
34 changed files with 2057 additions and 551 deletions
+4
View File
@@ -1,4 +1,5 @@
node_modules/ node_modules/
.pnpm-store/
data/ data/
logs/ logs/
*.db *.db
@@ -9,6 +10,9 @@ packages/web/dist
# Personal/local scripts and workflows (not for the repo) # Personal/local scripts and workflows (not for the repo)
debug-*.js debug-*.js
# Site-specific workflows (keep examples in workflows/default/)
packages/server/workflows/local/
# Plugin install staging and per-plugin deps # Plugin install staging and per-plugin deps
plugins/.staging-* plugins/.staging-*
plugins/*/node_modules/ plugins/*/node_modules/
+4 -3
View File
@@ -36,13 +36,14 @@ The first account created becomes **admin**. Later accounts are created from Use
| Kind | Name in YAML | Editable | Location | | Kind | Name in YAML | Editable | Location |
|---|---|---|---| |---|---|---|---|
| **Core** | `fetch-http.js`, `s3.js`, … | No (fork only) | `packages/server/scripts/` | | **Core** | `fetch-http.js`, `s3.js`, … | No (fork only) | `packages/server/scripts/` |
| **Plugin** | `plugin/<id>` | Yes | `plugins/<id>/` | | **User plugin** | `plugin/<id>` | Yes | `plugins/<id>/` |
| **Example source** | install → `plugin/<id>` | After install | `examples/plugins/<id>/` |
- App version is **`0.1.0`** (root `package.json`). Plugin manifests declare `jerapah: ">=0.1.0 <1.0.0"`. - App version is **`0.1.0`** (root `package.json`). Plugin manifests declare `jerapah: ">=0.1.0 <1.0.0"`.
- Install plugins via admin API: zip (base64), HTTPS git URL, example, or fork a core script. - Install plugins via admin API: zip (base64), HTTPS git URL, example, or fork a core script.
- Install/update/uninstall sets **restart-needed** — drain-restart HTTP + workers under `pnpm dev:pm2`. - Install/update/uninstall sets **restart-needed** — drain-restart HTTP + workers under `pnpm dev:pm2`.
- Example plugin: `examples/plugins/get-current-time` → `plugin/get-current-time`. - Shipped example source (install from UI/API): `examples/plugins/get-current-time` → runtime `plugin/get-current-time`.
- User plugins in this repo: `plugins/joplin-api` → `plugin/joplin-api`, `plugins/send-sms` → `plugin/send-sms`. - `plugins/joplin-api` and `plugins/send-sms` are **personal/user plugins** appropriate for a fork — not shipped examples. See **AGENTS.md** for creating user plugins under `plugins/<id>/`.
```bash ```bash
# Smoke # Smoke
+5
View File
@@ -1,3 +1,8 @@
/**
* Production PM2 ecosystem (monolith runner).
* Use for deployed/single-process starts. For local multi-process Ops UI, use
* `pnpm dev:pm2` → packages/server/ecosystem.dev.cjs / control.js instead.
*/
const fs = require("fs"); const fs = require("fs");
const path = require("path"); const path = require("path");
+3 -1
View File
@@ -14,7 +14,9 @@
"start:api": "pnpm --filter @jerapah-flow/server start:api", "start:api": "pnpm --filter @jerapah-flow/server start:api",
"start:worker": "pnpm --filter @jerapah-flow/server start:worker", "start:worker": "pnpm --filter @jerapah-flow/server start:worker",
"start:control": "pnpm --filter @jerapah-flow/server start:control", "start:control": "pnpm --filter @jerapah-flow/server start:control",
"migrate": "pnpm --filter @jerapah-flow/server migrate" "migrate": "pnpm --filter @jerapah-flow/server migrate",
"test": "pnpm --filter @jerapah-flow/shared test",
"lint": "pnpm --filter @jerapah-flow/web lint"
}, },
"packageManager": "pnpm@11.22.0" "packageManager": "pnpm@11.22.0"
} }
+2 -1
View File
@@ -1,7 +1,8 @@
/** /**
* Dev ecosystem for `pnpm dev:pm2`. * Local multi-process Ops UI ecosystem (`pnpm dev:pm2`).
* Prefer starting children via control.js (desired state) rather than this file. * Prefer starting children via control.js (desired state) rather than this file.
* Kept as a reference / fallback: `pm2 start packages/server/ecosystem.dev.cjs` * Kept as a reference / fallback: `pm2 start packages/server/ecosystem.dev.cjs`
* For production monolith, use root ecosystem.config.cjs instead.
*/ */
const path = require("path"); const path = require("path");
@@ -1,29 +0,0 @@
name: Joplin nightly daily log
description: |
POST joplin-auto /api/logs/run at 04:00 (sync + yearly/monthly/today), then ntfy.
Secret joplin_setup_token (SETUP_API_TOKEN). Variable ntfy_channel.
scripts:
- script: plugin/joplin-api
config:
url: http://10.8.0.6:3040/api/logs/run
method: POST
token: $SECRET_joplin_setup_token
timeoutMs: 600000
- set:
expression: |
{
"title": data.ok ? "Bullet journal" : "Bullet journal failed",
"message": data.ok
? "The bullet journal for " & data.httpResponse.date & " has been created."
: data.message
}
- script: ntfy.js
config:
url: $VAR_ntfy_channel
triggers:
- type: cron
schedule: "0 4 * * *"
- type: HTTP
method: POST
path: /dev-joplin-daily
enabled: false
@@ -1,17 +0,0 @@
name: Joplin get note
description: |
GET a Joplin note by id from joplin-api. Input (data): id (32-char hex).
Secret joplin_api_token (JOPLIN_API_TOKEN / API_KEYS). Returns the full note.
scripts:
- script: plugin/joplin-api
config:
url: http://10.8.0.6:3030/notes
method: GET
token: $SECRET_joplin_api_token
timeoutMs: 60000
triggers:
- type: workflow
- type: HTTP
method: POST
path: /dev-joplin-get-note
enabled: false
@@ -1,27 +0,0 @@
name: Joplin nightly sync
description: |
POST joplin-auto /api/sync at 03:00, then ntfy success or failure.
Secret joplin_setup_token (SETUP_API_TOKEN). Variable ntfy_channel.
scripts:
- script: plugin/joplin-api
config:
url: http://10.8.0.6:3040/api/sync
method: POST
token: $SECRET_joplin_setup_token
timeoutMs: 600000
- set:
expression: |
{
"title": data.ok ? "Joplin sync ok" : "Joplin sync failed",
"message": data.message
}
- script: ntfy.js
config:
url: $VAR_ntfy_channel
triggers:
- type: cron
schedule: "0 3 * * *"
- type: HTTP
method: POST
path: /dev-joplin-sync
enabled: false
@@ -1,17 +0,0 @@
name: dev-zte-sms
description: |
Send an SMS via a ZTE modem web UI.
Input (data): to, message
Password is the named secret zte_modem_password ($SECRET_).
scripts:
- script: plugin/send-sms
config:
url: http://192.168.5.1/reqproc/proc_post
password: $SECRET_sms_secret
triggers:
- type: workflow
- type: HTTP
method: POST
path: /dev-zte-sms
auth:
- 0f78d6d7-bd44-45d7-a826-f51c027b767f
@@ -1,12 +0,0 @@
name: fetch-devto
scripts:
- script: fetch-html.js
config:
url: https://dev.to/t/productivity/top/week
selector: "#substories h2"
outputVar: titles
jsonata: "$[].text"
triggers:
- type: HTTP
method: POST
path: /fetch-dev-to
@@ -1,50 +0,0 @@
name: Jadwal Solat Jakarta ntfy
scripts:
- id: fetch
script: fetch-http.js
config:
url: https://kemenag.go.id/api/prayer-times/1301
method: GET
headers:
Content-Type: application/json
Accept: application/json
- id: transform
script: jsonata.js
config:
expression: |-
{
"title": data.httpResponse.data.date,
"message": "Imsak:" & data.httpResponse.data.imsak & "\n" &
"Subuh:" & data.httpResponse.data.subuh & "\n" &
"Dzuhur:" & data.httpResponse.data.dzuhur & "\n" &
"Ashar:" & data.httpResponse.data.ashar & "\n" &
"Maghrib:" & data.httpResponse.data.maghrib & "\n" &
"Isya:" & data.httpResponse.data.isya
}
needs:
- fetch
- id: ntfy
script: ntfy.js
config:
url: $VAR_ntfy_channel
fingerprint: true
needs:
- transform
- id: slack
script: ntfy.js
config:
url: $VAR_ntfy_channel2
fingerprint: fingerprint:ntfy2
needs:
- transform
- script: slack-webhook.js
config:
webhookUrlSecret: slack_deploy_webhook
fingerprint: true
fingerprintMaxAge: 1h
text: $INPUT_message
needs:
- transform
triggers:
- type: cron
schedule: 0 5 * * *
@@ -1,20 +1,5 @@
scripts: scripts:
- time-to-ntfy-example.yaml
- test.yaml
- cron-example.yaml - cron-example.yaml
- fetch-devto.yaml
- comic-monkeyuser-to-ntfy.yaml
- time-and-comic-to-ntfy.yaml
- rss-selfhst-to-ntfy.yaml
- send-gmail.yaml
- test-send-gmail.yaml
- track.yaml
- rss-devto-to-ntfy.yaml
- test-minio.yaml
- dev-joplin-sync.yaml
- dev-joplin-daily.yaml
- dev-joplin-get-note.yaml
- web-dave.yaml
- jadwal-sholat-jakart.yaml
- detect-example-changes.yaml - detect-example-changes.yaml
- test-sftp.yaml - time-to-ntfy-example.yaml
- comic-monkeyuser-to-ntfy.yaml
@@ -1,27 +0,0 @@
name: RSS - selfh.st first item to ntfy (copy)
scripts:
- script: fetch-rss-feed.js
config:
url: https://selfh.st/rss/
outputVar: item
jsonata: items[0]
- script: fingerprint.js
config:
key: selfhst-latest
jsonata: "data.item.guid ? data.item.guid : data.item.link"
- script: jsonata.js
config:
expression: |
{
"title": data.item.title,
"message": data.item.contentSnippet & "\n" & data.item.link,
"attach": data.item.mediaContent.url ? data.item.mediaContent.url : (data.item.mediaContent.$ ? data.item.mediaContent.$.url : undefined)
}
- script: ntfy.js
config:
url: https://n.0dev.web.id/system
triggers:
- type: HTTP
method: POST
path: /selfhst-rss-rss-devto-to-ntfy
enabled: false
@@ -1,26 +0,0 @@
name: RSS - selfh.st first item to ntfy
scripts:
- script: fetch-rss-feed.js
config:
url: https://selfh.st/rss/
outputVar: item
jsonata: items[0]
- script: fingerprint.js
config:
key: selfhst-latest
jsonata: "data.item.guid ? data.item.guid : data.item.link"
- script: jsonata.js
config:
expression: |
{
"title": data.item.title,
"message": data.item.contentSnippet & "\n" & data.item.link,
"attach": data.item.mediaContent.url ? data.item.mediaContent.url : (data.item.mediaContent.$ ? data.item.mediaContent.$.url : undefined)
}
- script: ntfy.js
config:
url: https://n.0dev.web.id/system
triggers:
- type: HTTP
method: POST
path: /selfhst-rss
@@ -1,40 +0,0 @@
name: send-gmail
description: |
Send email via Gmail SMTP. Configure user/from and the named secret,
then call from other workflows with trigger-workflow.js (name: send-gmail).
Input (data):
to optional recipient(s); defaults to your Gmail below
subject required
text plain body (aliases: body, message)
html optional HTML body
from, cc, bcc, replyTo, priority, headers optional
scripts:
- script: jsonata.js
config:
expression: |
{
"to": $exists(data.to) ? data.to : "nasyarobby@gmail.com",
"from": data.from,
"subject": data.subject,
"text": $exists(data.text) ? data.text : ($exists(data.body) ? data.body : data.message),
"html": data.html,
"cc": data.cc,
"bcc": data.bcc,
"replyTo": data.replyTo,
"priority": data.priority,
"headers": data.headers
}
- script: send-email.js
config:
service: Gmail
user: elevent16th@gmail.com
from: elevent16th@gmail.com
passwordSecret: gmail_app_password
triggers:
- type: workflow
- type: HTTP
method: POST
path: /send-gmail
auth:
- 0f78d6d7-bd44-45d7-a826-f51c027b767f
@@ -1,20 +0,0 @@
name: Test MinIO
scripts:
- script: fetch-binary.js
config:
outputVar: file
url: https://nsrb:error403@dav.0dev.web.id/ntfy/IyM9784UdG4S
filename: test.png
- script: s3.js
config:
action: write
endpoint: http://localhost:9000
bucket: default
forcePathStyle: true
accessKeyIdSecret: minio_user
secretAccessKeySecret: minio_pass
key: file.png
triggers:
- type: HTTP
method: POST
path: /new
@@ -1,18 +0,0 @@
name: test-send-gmail
description: |
Kick send-gmail with sample data. Edit the payload below, then Run
(or POST /u/default/test-send-gmail).
scripts:
- script: trigger-workflow.js
config:
name: send-gmail
expression: |
{
"subject": "JerapahFlow test",
"text": "Hello from test-send-gmail",
"html": "<p>Hello from <strong>test-send-gmail</strong></p>"
}
triggers:
- type: HTTP
method: POST
path: /test-send-gmail
@@ -1,22 +0,0 @@
name: SFTP Test
scripts:
- script: remote-fs.js
config:
protocol: sftp
action: list
host: localhost
path: /Users/nsrb/
username: nsrb
port: 22
password: JKLjkl
- script: jsonata.js
config:
expression: '{"message": $join(data.entries.name, "\n")}'
- script: ntfy.js
config:
url: $VAR_ntfy_channel
fingerprint: "false"
triggers:
- type: HTTP
method: POST
path: /new
@@ -1,12 +0,0 @@
name: jsonata
scripts:
- plugin/get-current-time
- script: jsonata.js
config:
expression: '{"message": data.datetime & " " & data.processId}'
- ntfy.js
triggers:
- type: HTTP
method: POST
path: /mt
@@ -1,37 +0,0 @@
name: time and comic to ntfy
description: >
Fan-in from two scripts (current time + monkeyuser comic), then send to ntfy.
scripts:
- id: time
script: plugin/get-current-time
- id: comic
script: fetch-html.js
config:
url: "https://www.monkeyuser.com/"
outputVar: "httpResponse"
selector: ".comic img"
jsonata: |
{"url": "https://www.monkeyuser.com" & [attributes.src][0], "title": [attributes.title][0]}
- id: compose
script: jsonata.js
needs: [time, comic]
config:
expression: |
{
"title": data.comic.httpResponse.title,
"message": data.time.datetime & " " & data.comic.httpResponse.title,
"attach": data.comic.httpResponse.url
}
- id: notify
script: ntfy.js
needs: [compose]
config:
url: https://ntfy.sh/jerapah-flow
triggers:
- type: HTTP
method: POST
path: /time-and-comic
@@ -1,31 +0,0 @@
name: Track Gojek Trip
scripts:
- script: fetch-http.js
config:
url: https://api.gojekapi.com/live/track/491c3132c2cc7043
method: GET
headers:
Content-Type: application/json
Accept: application/json
Origin: https://track.gojek.com
- script: fingerprint.js
config:
key: gojek-track-status
jsonata: data.httpResponse.status
- script: jsonata.js
config:
expression: |
{
"title": data.httpResponse.customer_name,
"message": data.httpResponse.customer_name & " : " & data.httpResponse.status
}
- script: ntfy.js
config:
url: https://n.0dev.web.id/system
triggers:
- type: cron
schedule: "* * * * *"
- type: HTTP
method: GET
path: /new
enabled: false
@@ -1,14 +0,0 @@
name: Dab 0dev
scripts:
- script: list-webdav.js
config:
url: https://dav.0dev.web.id/books
path: /
includeDirectories: true
recursive: false
username: nsrb
passwordSecret: dav_0dev_password
triggers:
- type: HTTP
method: POST
path: /new
+6
View File
@@ -5,5 +5,11 @@
"type": "module", "type": "module",
"exports": { "exports": {
".": "./src/index.js" ".": "./src/index.js"
},
"scripts": {
"test": "vitest run"
},
"devDependencies": {
"vitest": "^3.2.4"
} }
} }
@@ -0,0 +1,58 @@
import { describe, expect, it } from "vitest";
import {
configHasOverlay,
ensureWorkflowFilename,
isPlainObject,
mergeProfileConfig,
namespacedPath,
} from "./index.js";
describe("isPlainObject", () => {
it("accepts plain objects and rejects arrays/null/primitives", () => {
expect(isPlainObject({})).toBe(true);
expect(isPlainObject({ a: 1 })).toBe(true);
expect(isPlainObject([])).toBe(false);
expect(isPlainObject(null)).toBe(false);
expect(isPlainObject("x")).toBe(false);
});
});
describe("mergeProfileConfig", () => {
it("shallow-merges step overlay over profile defaults", () => {
expect(mergeProfileConfig({ a: 1, b: 2 }, { b: 3, c: 4 })).toEqual({
a: 1,
b: 3,
c: 4,
});
expect(mergeProfileConfig(null, { x: 1 })).toEqual({ x: 1 });
expect(mergeProfileConfig({ x: 1 }, null)).toEqual({ x: 1 });
expect(mergeProfileConfig({ nested: { a: 1 } }, { nested: { b: 2 } })).toEqual({
nested: { b: 2 },
});
});
});
describe("configHasOverlay", () => {
it("is true only for non-empty plain objects", () => {
expect(configHasOverlay({ a: 1 })).toBe(true);
expect(configHasOverlay({})).toBe(false);
expect(configHasOverlay(null)).toBe(false);
expect(configHasOverlay([])).toBe(false);
});
});
describe("ensureWorkflowFilename", () => {
it("adds .yaml when missing and preserves yaml/yml", () => {
expect(ensureWorkflowFilename("cron")).toBe("cron.yaml");
expect(ensureWorkflowFilename("cron.yaml")).toBe("cron.yaml");
expect(ensureWorkflowFilename("cron.YML")).toBe("cron.YML");
expect(ensureWorkflowFilename(" ")).toBe("");
});
});
describe("namespacedPath", () => {
it("builds /u/<owner>/<path> without leading slash duplication", () => {
expect(namespacedPath("default", "hooks/run")).toBe("/u/default/hooks/run");
expect(namespacedPath("local", "/hooks/run")).toBe("/u/local/hooks/run");
});
});
+41
View File
@@ -0,0 +1,41 @@
import js from "@eslint/js";
import reactHooks from "eslint-plugin-react-hooks";
import jsxA11y from "eslint-plugin-jsx-a11y";
import globals from "globals";
export default [
{ ignores: ["dist/**"] },
{
files: ["src/**/*.{js,jsx}"],
languageOptions: {
ecmaVersion: "latest",
sourceType: "module",
globals: {
...globals.browser,
},
parserOptions: {
ecmaFeatures: { jsx: true },
},
},
plugins: {
"react-hooks": reactHooks,
"jsx-a11y": jsxA11y,
},
rules: {
...js.configs.recommended.rules,
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"jsx-a11y/alt-text": "warn",
"jsx-a11y/anchor-has-content": "warn",
"jsx-a11y/aria-role": "warn",
"no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^([A-Z_]|Lu)",
caughtErrorsIgnorePattern: "^_",
},
],
},
},
];
+7 -1
View File
@@ -6,7 +6,8 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",
"preview": "vite preview" "preview": "vite preview",
"lint": "eslint src"
}, },
"dependencies": { "dependencies": {
"@jerapah-flow/shared": "workspace:*", "@jerapah-flow/shared": "workspace:*",
@@ -26,9 +27,14 @@
"yaml": "^2.8.1" "yaml": "^2.8.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.33.0",
"@tailwindcss/vite": "^4.1.11", "@tailwindcss/vite": "^4.1.11",
"@vitejs/plugin-react": "^5.0.0", "@vitejs/plugin-react": "^5.0.0",
"daisyui": "^5.0.50", "daisyui": "^5.0.50",
"eslint": "^9.33.0",
"eslint-plugin-jsx-a11y": "^6.10.2",
"eslint-plugin-react-hooks": "^5.2.0",
"globals": "^16.3.0",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",
"vite": "^7.1.2" "vite": "^7.1.2"
} }
+15 -15
View File
@@ -1,4 +1,5 @@
import { LuCircleCheck, LuCircleX } from "react-icons/lu"; import { LuCircleCheck, LuCircleX } from "react-icons/lu";
import { OpsStatusCard } from "./OpsStatusCard.jsx";
export function HttpProcessCard({ export function HttpProcessCard({
httpOnline, httpOnline,
@@ -8,19 +9,10 @@ export function HttpProcessCard({
stopPending = false, stopPending = false,
}) { }) {
return ( return (
<div className="card bg-base-100 border border-base-300 shadow-sm"> <OpsStatusCard
<div className="card-body gap-4"> title="HTTP process"
<h2 className="card-title text-lg">HTTP process</h2> actions={
<div className="flex items-center gap-2"> httpOnline ? (
{httpOnline ? (
<LuCircleCheck className="size-6 text-success" aria-hidden />
) : (
<LuCircleX className="size-6 text-error" aria-hidden />
)}
<span>{httpOnline ? "Running" : "Stopped"}</span>
</div>
<div className="card-actions justify-start px-0">
{httpOnline ? (
<button <button
type="button" type="button"
className="btn btn-sm btn-warning" className="btn btn-sm btn-warning"
@@ -38,9 +30,17 @@ export function HttpProcessCard({
> >
Start HTTP Start HTTP
</button> </button>
)
}
>
<div className="flex items-center gap-2">
{httpOnline ? (
<LuCircleCheck className="size-6 text-success" aria-hidden />
) : (
<LuCircleX className="size-6 text-error" aria-hidden />
)} )}
<span>{httpOnline ? "Running" : "Stopped"}</span>
</div> </div>
</div> </OpsStatusCard>
</div>
); );
} }
@@ -0,0 +1,11 @@
export function OpsStatusCard({ title, children, actions }) {
return (
<div className="card bg-base-100 border border-base-300 shadow-sm">
<div className="card-body gap-4">
<h2 className="card-title text-lg">{title}</h2>
{children}
{actions ? <div className="card-actions justify-start px-0">{actions}</div> : null}
</div>
</div>
);
}
+26 -26
View File
@@ -1,4 +1,5 @@
import { LuCircleCheck, LuCirclePause } from "react-icons/lu"; import { LuCircleCheck, LuCirclePause } from "react-icons/lu";
import { OpsStatusCard } from "./OpsStatusCard.jsx";
export function QueueCard({ export function QueueCard({
paused = false, paused = false,
@@ -11,9 +12,30 @@ export function QueueCard({
resumePending = false, resumePending = false,
}) { }) {
return ( return (
<div className="card bg-base-100 border border-base-300 shadow-sm"> <OpsStatusCard
<div className="card-body gap-4"> title="Queue"
<h2 className="card-title text-lg">Queue</h2> actions={
paused ? (
<button
type="button"
className="btn btn-sm btn-primary"
disabled={resumePending}
onClick={onResume}
>
Resume workers
</button>
) : (
<button
type="button"
className="btn btn-sm btn-warning"
disabled={pausePending}
onClick={onPause}
>
Pause workers
</button>
)
}
>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{paused ? ( {paused ? (
<LuCirclePause className="size-6 text-warning" aria-hidden /> <LuCirclePause className="size-6 text-warning" aria-hidden />
@@ -34,28 +56,6 @@ export function QueueCard({
{queuedJobs} queued job{queuedJobs === 1 ? "" : "s"} {queuedJobs} queued job{queuedJobs === 1 ? "" : "s"}
</p> </p>
</div> </div>
<div className="card-actions justify-start px-0"> </OpsStatusCard>
{paused ? (
<button
type="button"
className="btn btn-sm btn-primary"
disabled={resumePending}
onClick={onResume}
>
Resume workers
</button>
) : (
<button
type="button"
className="btn btn-sm btn-warning"
disabled={pausePending}
onClick={onPause}
>
Pause workers
</button>
)}
</div>
</div>
</div>
); );
} }
+1 -1
View File
@@ -39,7 +39,7 @@ function parentIdsFromNeeds(needs) {
function mermaidLabel(text) { function mermaidLabel(text) {
return String(text ?? "") return String(text ?? "")
.replace(/"/g, "#quot;") .replace(/"/g, "#quot;")
.replace(/[\[\]]/g, " ") .replace(/[[\]]/g, " ")
.trim() || "node"; .trim() || "node";
} }
@@ -1,8 +0,0 @@
{
"id": "get-current-time",
"name": "Get current time",
"version": "0.1.0",
"jerapah": ">=0.1.0 <1.0.0",
"main": "script.js",
"description": "Example user plugin: return the current time as output.datetime"
}
-7
View File
@@ -1,7 +0,0 @@
{
"name": "jflow-plugin-get-current-time",
"version": "0.1.0",
"private": true,
"type": "module",
"description": "Example JerapahFlow plugin (no npm dependencies)"
}
-34
View File
@@ -1,34 +0,0 @@
function passContext(ctx) {
if (ctx?.context != null && typeof ctx.context === "object" && !Array.isArray(ctx.context)) {
return { ...ctx.context };
}
return {};
}
function getCurrentTime(ctx) {
const output = {
datetime: new Date().toISOString(),
processId: "1234",
};
return { output, context: { ...passContext(ctx), ...output } };
}
getCurrentTime.meta = {
description: "Return the current time as output.datetime",
config: {},
input: {},
output: {
datetime: { type: "string", description: "ISO timestamp" },
processId: { type: "string" },
},
context: {
datetime: { type: "string", description: "ISO timestamp" },
processId: { type: "string" },
},
example: {
data: {},
config: {},
},
};
export default getCurrentTime;
+1835 -1
View File
File diff suppressed because it is too large Load Diff