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:
@@ -1,4 +1,5 @@
|
||||
node_modules/
|
||||
.pnpm-store/
|
||||
data/
|
||||
logs/
|
||||
*.db
|
||||
@@ -9,6 +10,9 @@ packages/web/dist
|
||||
# Personal/local scripts and workflows (not for the repo)
|
||||
debug-*.js
|
||||
|
||||
# Site-specific workflows (keep examples in workflows/default/)
|
||||
packages/server/workflows/local/
|
||||
|
||||
# Plugin install staging and per-plugin deps
|
||||
plugins/.staging-*
|
||||
plugins/*/node_modules/
|
||||
|
||||
@@ -36,13 +36,14 @@ The first account created becomes **admin**. Later accounts are created from Use
|
||||
| Kind | Name in YAML | Editable | Location |
|
||||
|---|---|---|---|
|
||||
| **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"`.
|
||||
- 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`.
|
||||
- Example plugin: `examples/plugins/get-current-time` → `plugin/get-current-time`.
|
||||
- User plugins in this repo: `plugins/joplin-api` → `plugin/joplin-api`, `plugins/send-sms` → `plugin/send-sms`.
|
||||
- Shipped example source (install from UI/API): `examples/plugins/get-current-time` → runtime `plugin/get-current-time`.
|
||||
- `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
|
||||
# Smoke
|
||||
|
||||
@@ -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 path = require("path");
|
||||
|
||||
|
||||
+3
-1
@@ -14,7 +14,9 @@
|
||||
"start:api": "pnpm --filter @jerapah-flow/server start:api",
|
||||
"start:worker": "pnpm --filter @jerapah-flow/server start:worker",
|
||||
"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"
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
* 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");
|
||||
|
||||
|
||||
@@ -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:
|
||||
- time-to-ntfy-example.yaml
|
||||
- test.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
|
||||
- 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
|
||||
@@ -5,5 +5,11 @@
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./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");
|
||||
});
|
||||
});
|
||||
@@ -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: "^_",
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
];
|
||||
@@ -6,7 +6,8 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint src"
|
||||
},
|
||||
"dependencies": {
|
||||
"@jerapah-flow/shared": "workspace:*",
|
||||
@@ -26,9 +27,14 @@
|
||||
"yaml": "^2.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.33.0",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@vitejs/plugin-react": "^5.0.0",
|
||||
"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",
|
||||
"vite": "^7.1.2"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { LuCircleCheck, LuCircleX } from "react-icons/lu";
|
||||
import { OpsStatusCard } from "./OpsStatusCard.jsx";
|
||||
|
||||
export function HttpProcessCard({
|
||||
httpOnline,
|
||||
@@ -8,19 +9,10 @@ export function HttpProcessCard({
|
||||
stopPending = false,
|
||||
}) {
|
||||
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">HTTP process</h2>
|
||||
<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 className="card-actions justify-start px-0">
|
||||
{httpOnline ? (
|
||||
<OpsStatusCard
|
||||
title="HTTP process"
|
||||
actions={
|
||||
httpOnline ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-sm btn-warning"
|
||||
@@ -38,9 +30,17 @@ export function HttpProcessCard({
|
||||
>
|
||||
Start HTTP
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import { LuCircleCheck, LuCirclePause } from "react-icons/lu";
|
||||
import { OpsStatusCard } from "./OpsStatusCard.jsx";
|
||||
|
||||
export function QueueCard({
|
||||
paused = false,
|
||||
@@ -11,9 +12,30 @@ export function QueueCard({
|
||||
resumePending = false,
|
||||
}) {
|
||||
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">Queue</h2>
|
||||
<OpsStatusCard
|
||||
title="Queue"
|
||||
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">
|
||||
{paused ? (
|
||||
<LuCirclePause className="size-6 text-warning" aria-hidden />
|
||||
@@ -34,28 +56,6 @@ export function QueueCard({
|
||||
{queuedJobs} queued job{queuedJobs === 1 ? "" : "s"}
|
||||
</p>
|
||||
</div>
|
||||
<div className="card-actions justify-start px-0">
|
||||
{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>
|
||||
</OpsStatusCard>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ function parentIdsFromNeeds(needs) {
|
||||
function mermaidLabel(text) {
|
||||
return String(text ?? "")
|
||||
.replace(/"/g, "#quot;")
|
||||
.replace(/[\[\]]/g, " ")
|
||||
.replace(/[[\]]/g, " ")
|
||||
.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"
|
||||
}
|
||||
@@ -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)"
|
||||
}
|
||||
@@ -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;
|
||||
Generated
+1835
-1
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user