bullpane 0.5.2 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/bin/bullpane.mjs +28 -1
- package/dist/lua/searchJobs.lua +37 -7
- package/dist/server.mjs +1535 -111
- package/migrations/mysql/0010_connection_kind.sql +4 -0
- package/migrations/sqlite/0003_connection_kind.sql +3 -0
- package/package.json +5 -2
- package/web/assets/{FlowsPage-CxMz_9Et.js → FlowsPage-BAChK8uT.js} +1 -1
- package/web/assets/{JobTreePage-BT79W1zX.js → JobTreePage-Vvq6aoQU.js} +1 -1
- package/web/assets/{McpConsentPage-B2YnL6fN.js → McpConsentPage-BHmbIbrR.js} +1 -1
- package/web/assets/{flowLayout-DoC2dCG0.js → flowLayout-g8b3XZHS.js} +1 -1
- package/web/assets/index-BuZ0fAkt.js +548 -0
- package/web/index.html +1 -1
- package/web/assets/index-DuAopWKm.js +0 -543
package/README.md
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
npx bullpane --redis redis://localhost:6379
|
|
7
|
+
# or, for BullMQ 6's Postgres backend
|
|
8
|
+
npx bullpane --postgres postgres://user:pass@localhost:5432/app
|
|
7
9
|
```
|
|
8
10
|
|
|
9
11
|
Open <http://localhost:3000>. That is the whole install: no database to set up, no account, no login.
|
|
@@ -14,24 +16,26 @@ Open <http://localhost:3000>. That is the whole install: no database to set up,
|
|
|
14
16
|
|
|
15
17
|
## What you get
|
|
16
18
|
|
|
17
|
-
- Every queue on a Redis, with counts, success rate and what needs attention.
|
|
19
|
+
- Every queue on a Redis, or in BullMQ 6's Postgres backend, with counts, success rate and what needs attention.
|
|
18
20
|
- Jobs by state, search inside job data, job detail with data, return value, stack trace and logs.
|
|
19
21
|
- Retry, promote, remove and bulk actions through the official `bullmq` API. Pause, resume, clean, drain.
|
|
20
22
|
- Job schedulers, flows (parent/child trees), stalled jobs, Redis health.
|
|
21
23
|
- BullMQ Pro groups: per-group concurrency, rate limits and paused groups.
|
|
22
24
|
- Safe on a busy production Redis: no `KEYS`, one round trip per read, payloads truncated inside Redis.
|
|
23
25
|
|
|
24
|
-
Works with BullMQ 4, 5 and 6 on Redis, Redis Cluster and Valkey, and with BullMQ Pro.
|
|
26
|
+
Works with BullMQ 4, 5 and 6 on Redis, Redis Cluster and Valkey, with BullMQ 6 on Postgres (behind PgBouncer too), and with BullMQ Pro.
|
|
25
27
|
|
|
26
28
|
**Pro** (USD 39/month, one installation, unlimited users) adds login with roles, SSO, alerts to Slack or webhooks, folders, a flow graph, an audit log and an MCP server for Claude and other AI clients.
|
|
27
29
|
|
|
28
30
|
## Options
|
|
29
31
|
|
|
30
32
|
```
|
|
31
|
-
npx bullpane [--redis <url>] [options]
|
|
33
|
+
npx bullpane [--redis <url> | --postgres <url>] [options]
|
|
32
34
|
|
|
33
35
|
--redis <url> Redis your BullMQ workers use, added as a connection
|
|
34
36
|
--prefix <prefix> BullMQ prefix of that connection (default: bull)
|
|
37
|
+
--postgres <url> Postgres of BullMQ 6's Postgres backend, added as a connection
|
|
38
|
+
--schema <schema> Schema of that connection (default: bullmq)
|
|
35
39
|
--port <port> HTTP port (default: 3000)
|
|
36
40
|
--host <host> Interface to listen on (default: 127.0.0.1)
|
|
37
41
|
--data-dir <dir> Where the SQLite database lives (default: ~/.bullpane)
|
package/bin/bullpane.mjs
CHANGED
|
@@ -18,12 +18,15 @@ const { version } = JSON.parse(readFileSync(path.join(root, "package.json"), "ut
|
|
|
18
18
|
const HELP = `Bullpane ${version} — self-hosted dashboard for BullMQ and BullMQ Pro
|
|
19
19
|
|
|
20
20
|
Usage
|
|
21
|
-
npx bullpane [--redis <url>] [options]
|
|
21
|
+
npx bullpane [--redis <url> | --postgres <url>] [options]
|
|
22
22
|
|
|
23
23
|
Options
|
|
24
24
|
--redis <url> Redis your BullMQ workers use, added as a connection
|
|
25
25
|
(e.g. redis://localhost:6379). More can be added in the UI.
|
|
26
26
|
--prefix <prefix> BullMQ prefix of that connection (default: bull)
|
|
27
|
+
--postgres <url> Postgres of BullMQ 6's Postgres backend, added as a
|
|
28
|
+
connection (e.g. postgres://user:pass@localhost:5432/app)
|
|
29
|
+
--schema <schema> Schema BullMQ created its tables in (default: bullmq)
|
|
27
30
|
--port <port> HTTP port (default: 3000, or $PORT)
|
|
28
31
|
--host <host> Interface to listen on (default: 127.0.0.1). The free
|
|
29
32
|
edition has no login: only use 0.0.0.0 on a private network.
|
|
@@ -41,6 +44,8 @@ try {
|
|
|
41
44
|
options: {
|
|
42
45
|
redis: { type: "string" },
|
|
43
46
|
prefix: { type: "string" },
|
|
47
|
+
postgres: { type: "string" },
|
|
48
|
+
schema: { type: "string" },
|
|
44
49
|
port: { type: "string" },
|
|
45
50
|
host: { type: "string" },
|
|
46
51
|
"data-dir": { type: "string" },
|
|
@@ -82,6 +87,28 @@ set("WEB_DIST", undefined, path.join(root, "web"));
|
|
|
82
87
|
set("PUBLIC_URL", undefined, `http://localhost:${env.PORT}`);
|
|
83
88
|
set("NODE_ENV", undefined, "production");
|
|
84
89
|
|
|
90
|
+
if (args.redis && args.postgres) {
|
|
91
|
+
console.error("--redis and --postgres are one connection each: pass one, add the other in the UI");
|
|
92
|
+
process.exit(2);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** host:port, never the password */
|
|
96
|
+
const nameFrom = (url) => {
|
|
97
|
+
const u = new URL(url);
|
|
98
|
+
return `${u.hostname}${u.port ? `:${u.port}` : ""}`;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
if (args.postgres) {
|
|
102
|
+
let name;
|
|
103
|
+
try {
|
|
104
|
+
name = nameFrom(args.postgres);
|
|
105
|
+
} catch {
|
|
106
|
+
console.error(`--postgres must be a URL like postgres://user:pass@localhost:5432/app`);
|
|
107
|
+
process.exit(2);
|
|
108
|
+
}
|
|
109
|
+
env.BULLPANE_CONNECTIONS = JSON.stringify([{ name, kind: "postgres", url: args.postgres, prefix: args.schema ?? "bullmq" }]);
|
|
110
|
+
}
|
|
111
|
+
|
|
85
112
|
if (args.redis) {
|
|
86
113
|
let name = "Redis";
|
|
87
114
|
try {
|
package/dist/lua/searchJobs.lua
CHANGED
|
@@ -16,7 +16,10 @@
|
|
|
16
16
|
ARGV[9] byteBudget: stop the call once this many payload bytes were copied
|
|
17
17
|
into Lua, even if `batch` hashes were not reached. The cursor comes
|
|
18
18
|
back as usual and the UI keeps streaming.
|
|
19
|
-
ARGV[10
|
|
19
|
+
ARGV[10] group id (BullMQ Pro), or "" for no group filter
|
|
20
|
+
ARGV[11] the hash fields that may hold the group id, comma separated
|
|
21
|
+
(GROUP_ID_FIELDS in keys.ts); `opts.group.id` is the fallback
|
|
22
|
+
ARGV[12..] hash fields to HMGET (must include name, data, failedReason)
|
|
20
23
|
|
|
21
24
|
Returns { matches, nextCursor, scanned, total, skipped }
|
|
22
25
|
matches rows shaped exactly like getJobs.lua rows (…, dataTruncated, dataBytes)
|
|
@@ -30,6 +33,11 @@
|
|
|
30
33
|
call independent of both queue size and payload size: at most `batch` hashes
|
|
31
34
|
and at most `byteBudget` payload bytes. Before them a search over 1000 × 1 MB
|
|
32
35
|
jobs kept Redis busy for 13 s; with them a call is a few milliseconds.
|
|
36
|
+
|
|
37
|
+
With a group id, a job outside the group costs one HMGET of its group fields
|
|
38
|
+
and opts (a few hundred bytes, counted in the byte budget) and its payload is
|
|
39
|
+
never read. Pro keeps a group's delayed, failed and completed jobs in the
|
|
40
|
+
queue-wide state keys, so this is the only way to list them per group.
|
|
33
41
|
]]
|
|
34
42
|
local rcall = redis.call
|
|
35
43
|
local key = KEYS[1]
|
|
@@ -42,9 +50,14 @@ local qprefix = ARGV[6]
|
|
|
42
50
|
local previewBytes = tonumber(ARGV[7])
|
|
43
51
|
local maxFieldBytes = tonumber(ARGV[8])
|
|
44
52
|
local byteBudget = tonumber(ARGV[9])
|
|
53
|
+
local group = ARGV[10]
|
|
54
|
+
|
|
55
|
+
local groupFields = {}
|
|
56
|
+
for f in string.gmatch(ARGV[11], "[^,]+") do groupFields[#groupFields + 1] = f end
|
|
57
|
+
groupFields[#groupFields + 1] = "opts"
|
|
45
58
|
|
|
46
59
|
local fields = {}
|
|
47
|
-
for i =
|
|
60
|
+
for i = 12, #ARGV do fields[#fields + 1] = ARGV[i] end
|
|
48
61
|
local SKIP = "\0skip"
|
|
49
62
|
|
|
50
63
|
local dataIdx, retIdx, nameIdx, reasonIdx = nil, nil, nil, nil
|
|
@@ -83,9 +96,25 @@ local function has(hay, needle)
|
|
|
83
96
|
return hay and string.find(string.lower(hay), needle, 1, true) ~= nil
|
|
84
97
|
end
|
|
85
98
|
|
|
99
|
+
-- Exact group match: a group id field on the hash, else opts.group.id. opts is
|
|
100
|
+
-- only decoded when it contains the id as plain text, so most misses cost a find.
|
|
101
|
+
local function inGroup(hkey)
|
|
102
|
+
local vals = rcall("HMGET", hkey, unpack(groupFields))
|
|
103
|
+
local opts = vals[#vals]
|
|
104
|
+
if opts then bytes = bytes + #opts end
|
|
105
|
+
for i = 1, #vals - 1 do
|
|
106
|
+
if vals[i] then return vals[i] == group end
|
|
107
|
+
end
|
|
108
|
+
if not opts or not string.find(opts, group, 1, true) then return false end
|
|
109
|
+
local ok, decoded = pcall(cjson.decode, opts)
|
|
110
|
+
if not ok or type(decoded) ~= "table" or type(decoded.group) ~= "table" then return false end
|
|
111
|
+
local id = decoded.group.id
|
|
112
|
+
return id ~= nil and tostring(id) == group
|
|
113
|
+
end
|
|
114
|
+
|
|
86
115
|
for _, id in ipairs(ids) do
|
|
87
116
|
scanned = scanned + 1
|
|
88
|
-
if string.sub(id, 1, 2) ~= "0:" then
|
|
117
|
+
if string.sub(id, 1, 2) ~= "0:" and (group == "" or inGroup(qprefix .. id)) then
|
|
89
118
|
local hkey = qprefix .. id
|
|
90
119
|
local dataBytes = dataIdx and rcall("HSTRLEN", hkey, "data") or 0
|
|
91
120
|
local retBytes = retIdx and rcall("HSTRLEN", hkey, "returnvalue") or 0
|
|
@@ -129,12 +158,13 @@ for _, id in ipairs(ids) do
|
|
|
129
158
|
break
|
|
130
159
|
end
|
|
131
160
|
end
|
|
132
|
-
if bytes >= byteBudget then
|
|
133
|
-
stoppedEarly = true
|
|
134
|
-
break
|
|
135
|
-
end
|
|
136
161
|
end
|
|
137
162
|
end
|
|
163
|
+
-- Also after a group miss: those read opts, which counts against the budget.
|
|
164
|
+
if bytes >= byteBudget then
|
|
165
|
+
stoppedEarly = true
|
|
166
|
+
break
|
|
167
|
+
end
|
|
138
168
|
end
|
|
139
169
|
|
|
140
170
|
local nextCursor = cursor + scanned
|