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/
|
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/
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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": {
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
@@ -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