bullpane 0.6.2 → 0.6.3
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/dist/lua/queueStats.lua +26 -0
- package/dist/server.mjs +152 -5
- package/package.json +1 -1
- package/web/assets/{FlowsPage-CE4Fl11b.js → FlowsPage-BVM22MG9.js} +1 -1
- package/web/assets/{JobTreePage-CxJzFwh9.js → JobTreePage-BZbwFo2w.js} +1 -1
- package/web/assets/{McpConsentPage-CmZMfyo4.js → McpConsentPage-o4BOZhJd.js} +1 -1
- package/web/assets/{flowLayout-GUFMVhrV.js → flowLayout-Bwc9MVRI.js} +1 -1
- package/web/assets/index-B5JQkwxp.css +1 -0
- package/web/assets/index-DS8k2gIv.js +553 -0
- package/web/index.html +2 -2
- package/web/assets/index-C2gL_lbF.css +0 -1
- package/web/assets/index-CN2nADIL.js +0 -548
package/dist/lua/queueStats.lua
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
ARGV[2] number of metric points (newest N minutes)
|
|
17
17
|
ARGV[3] window start (unix ms) for the success/failure rate ZCOUNTs
|
|
18
18
|
ARGV[4] `${prefix}:${queue}:` (to read a job's opts and detect retention)
|
|
19
|
+
ARGV[5] max groups whose waiting jobs are summed into [22]; 0 = skip (lists)
|
|
19
20
|
|
|
20
21
|
Returns a flat array:
|
|
21
22
|
[1..8] counts (LLEN for lists, ZCARD for zsets)
|
|
@@ -37,6 +38,10 @@
|
|
|
37
38
|
in the same EVALSHA (no extra round trip to Redis). Worth the budget
|
|
38
39
|
because it is the only way for the `active` tab to say "3 of these hung";
|
|
39
40
|
without it the operator sees "active 8" and cannot tell half are dead.
|
|
41
|
+
[22] BullMQ Pro: jobs waiting in groups (each group's list + its prioritized
|
|
42
|
+
zset), summed over at most ARGV[5] groups. Pro keeps them there, not in
|
|
43
|
+
`wait`, so [1] alone says 0 for a Pro queue full of grouped jobs.
|
|
44
|
+
[23] how many groups were summed into [22]; fewer than [11] = partial sum.
|
|
40
45
|
|
|
41
46
|
Cluster safe: every key belongs to the same queue (same hash tag).
|
|
42
47
|
Read only: the legacy "0:" wait-list marker is skipped, never popped.
|
|
@@ -137,4 +142,25 @@ out[20] = rcall("ZCARD", KEYS[15])
|
|
|
137
142
|
-- hung. SCARD is O(1), so the count rides along for free in this script.
|
|
138
143
|
out[21] = rcall("SCARD", KEYS[16])
|
|
139
144
|
|
|
145
|
+
-- Waiting jobs of BullMQ Pro groups: two O(1) commands per group (LLEN + ZCARD),
|
|
146
|
+
-- walking the four status zsets in Pro's order and stopping at ARGV[5] groups, so
|
|
147
|
+
-- the cost is bounded however many groups a queue has. Only the single-queue page
|
|
148
|
+
-- asks (ARGV[5] = 0 everywhere else), and only a queue with groups pays.
|
|
149
|
+
local groupWaiting, groupsSummed = 0, 0
|
|
150
|
+
local maxGroups = tonumber(ARGV[5]) or 0
|
|
151
|
+
if maxGroups > 0 and groupsCount > 0 then
|
|
152
|
+
local statusKeys = { KEYS[10], ARGV[4] .. "groups:limit", ARGV[4] .. "groups:max", ARGV[4] .. "groups:paused" }
|
|
153
|
+
for _, statusKey in ipairs(statusKeys) do
|
|
154
|
+
local left = maxGroups - groupsSummed
|
|
155
|
+
if left <= 0 then break end
|
|
156
|
+
for _, gid in ipairs(rcall("ZRANGE", statusKey, 0, left - 1)) do
|
|
157
|
+
local groupKey = ARGV[4] .. "groups:" .. gid
|
|
158
|
+
groupWaiting = groupWaiting + rcall("LLEN", groupKey) + rcall("ZCARD", groupKey .. ":p")
|
|
159
|
+
groupsSummed = groupsSummed + 1
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
out[22] = groupWaiting
|
|
164
|
+
out[23] = groupsSummed
|
|
165
|
+
|
|
140
166
|
return out
|
package/dist/server.mjs
CHANGED
|
@@ -201,6 +201,7 @@ var JOB_STATES = [
|
|
|
201
201
|
"waiting-children"
|
|
202
202
|
];
|
|
203
203
|
var jobStateSchema = z.enum(JOB_STATES);
|
|
204
|
+
var GROUP_WAITING_CAP = 1e3;
|
|
204
205
|
var GROUP_STATUSES = ["waiting", "limited", "maxed", "paused"];
|
|
205
206
|
var listSchedulersQuerySchema = z.object({
|
|
206
207
|
page: z.coerce.number().int().min(1).default(1),
|
|
@@ -237,6 +238,12 @@ var BULK_JOB_ACTIONS = ["retry", "remove", "promote"];
|
|
|
237
238
|
var bulkJobActionSchema = z.object({
|
|
238
239
|
jobIds: z.array(z.string().min(1).max(200)).min(1, "jobIds must not be empty").max(BULK_JOB_LIMIT, `at most ${BULK_JOB_LIMIT} job ids per call`)
|
|
239
240
|
});
|
|
241
|
+
var PROMOTE_MATCHING_LIMIT = 2e3;
|
|
242
|
+
var promoteMatchingSchema = z.object({
|
|
243
|
+
query: z.string().max(500).optional(),
|
|
244
|
+
groupId: z.string().min(1).max(200).optional(),
|
|
245
|
+
cursor: z.string().max(20).optional()
|
|
246
|
+
}).refine((v) => !!v.query?.trim() || v.groupId !== void 0, { message: "query or groupId is required", path: ["query"] });
|
|
240
247
|
var listJobsQuerySchema = z.object({
|
|
241
248
|
state: jobStateSchema.default("waiting"),
|
|
242
249
|
/** BullMQ Pro: restrict to a group's waiting list (state is ignored when set) */
|
|
@@ -376,6 +383,7 @@ var AUDIT_ACTIONS = [
|
|
|
376
383
|
"queue.drain",
|
|
377
384
|
"queue.obliterate",
|
|
378
385
|
"scheduler.remove",
|
|
386
|
+
"job.promote_matching",
|
|
379
387
|
// BullMQ Pro groups
|
|
380
388
|
"group.pause",
|
|
381
389
|
"group.resume",
|
|
@@ -1049,6 +1057,7 @@ function proApiRequired(what, consequence) {
|
|
|
1049
1057
|
var SCAN_COUNT = 1e3;
|
|
1050
1058
|
var STATS_METRIC_POINTS = 60;
|
|
1051
1059
|
var BULK_CONCURRENCY = 8;
|
|
1060
|
+
var PROMOTE_MATCHING_MAX_CALLS = 50;
|
|
1052
1061
|
var DEFAULT_RATE_WINDOW_MINUTES = 60;
|
|
1053
1062
|
var FORCE_DISCOVERY_MIN_MS = 5e3;
|
|
1054
1063
|
var SETUP_CACHE_MS = 1e4;
|
|
@@ -1387,17 +1396,18 @@ var RedisInspector = class {
|
|
|
1387
1396
|
const withMetrics = opts.withMetrics ? 1 : 0;
|
|
1388
1397
|
const windowMinutes = opts.rateWindowMinutes ?? DEFAULT_RATE_WINDOW_MINUTES;
|
|
1389
1398
|
const since = Date.now() - windowMinutes * 6e4;
|
|
1399
|
+
const groupCap = Math.max(0, Math.trunc(opts.groupWaitingCap ?? 0));
|
|
1390
1400
|
let replies;
|
|
1391
1401
|
if (c instanceof Cluster2) {
|
|
1392
1402
|
replies = await Promise.all(
|
|
1393
1403
|
queueNames.map(
|
|
1394
|
-
(q) => callScript(c, "queueStats", [...this.statsKeys(q), withMetrics, STATS_METRIC_POINTS, since, queueKeyPrefix(this.config.prefix, q)]).catch(() => null)
|
|
1404
|
+
(q) => callScript(c, "queueStats", [...this.statsKeys(q), withMetrics, STATS_METRIC_POINTS, since, queueKeyPrefix(this.config.prefix, q), groupCap]).catch(() => null)
|
|
1395
1405
|
)
|
|
1396
1406
|
);
|
|
1397
1407
|
} else {
|
|
1398
1408
|
const pipeline = c.pipeline();
|
|
1399
1409
|
for (const q of queueNames) {
|
|
1400
|
-
pipelineScript(pipeline, "queueStats", [...this.statsKeys(q), withMetrics, STATS_METRIC_POINTS, since, queueKeyPrefix(this.config.prefix, q)]);
|
|
1410
|
+
pipelineScript(pipeline, "queueStats", [...this.statsKeys(q), withMetrics, STATS_METRIC_POINTS, since, queueKeyPrefix(this.config.prefix, q), groupCap]);
|
|
1401
1411
|
}
|
|
1402
1412
|
const results = await pipeline.exec() ?? [];
|
|
1403
1413
|
replies = results.map(([err, reply]) => err ? null : reply);
|
|
@@ -1405,7 +1415,7 @@ var RedisInspector = class {
|
|
|
1405
1415
|
queueNames.forEach((name, i) => {
|
|
1406
1416
|
const reply = replies[i];
|
|
1407
1417
|
if (reply === null || reply === void 0) return;
|
|
1408
|
-
out[name] = parseStats(reply, withMetrics === 1, windowMinutes);
|
|
1418
|
+
out[name] = parseStats(reply, withMetrics === 1, windowMinutes, groupCap > 0);
|
|
1409
1419
|
});
|
|
1410
1420
|
return out;
|
|
1411
1421
|
}
|
|
@@ -1550,6 +1560,45 @@ var RedisInspector = class {
|
|
|
1550
1560
|
skippedLargePayloads: asNumber(reply[4])
|
|
1551
1561
|
};
|
|
1552
1562
|
}
|
|
1563
|
+
/**
|
|
1564
|
+
* The search script over `delayed`, with payload previews cut to 0 bytes: only
|
|
1565
|
+
* the ids are needed. Runs slice after slice until `limit` matches or
|
|
1566
|
+
* PROMOTE_MATCHING_MAX_CALLS calls, so one request stays bounded on a state of
|
|
1567
|
+
* millions. Each slice is the same bounded call the search box makes.
|
|
1568
|
+
*/
|
|
1569
|
+
async scanDelayedIds(queueName, match, cursor, limit) {
|
|
1570
|
+
const c = await this.ensureConnected();
|
|
1571
|
+
const ids = [];
|
|
1572
|
+
let scanned = 0;
|
|
1573
|
+
let total = 0;
|
|
1574
|
+
let at = cursor;
|
|
1575
|
+
for (let call = 0; call < PROMOTE_MATCHING_MAX_CALLS && ids.length < limit; call++) {
|
|
1576
|
+
const reply = asArray(
|
|
1577
|
+
await callScript(c, "getJobsSearch", [
|
|
1578
|
+
stateKey(this.config.prefix, queueName, "delayed"),
|
|
1579
|
+
STATE_KEY.delayed.type,
|
|
1580
|
+
at,
|
|
1581
|
+
match.groupId && !match.query ? this.opts.groupScanPerCall : this.opts.maxScanPerCall,
|
|
1582
|
+
match.query.toLowerCase(),
|
|
1583
|
+
limit - ids.length,
|
|
1584
|
+
queueKeyPrefix(this.config.prefix, queueName),
|
|
1585
|
+
0,
|
|
1586
|
+
this.opts.searchFieldCapBytes,
|
|
1587
|
+
this.opts.searchByteBudget,
|
|
1588
|
+
match.groupId,
|
|
1589
|
+
GROUP_ID_FIELDS.join(","),
|
|
1590
|
+
...JOB_SUMMARY_FIELDS
|
|
1591
|
+
])
|
|
1592
|
+
);
|
|
1593
|
+
for (const row of asArray(reply[0])) ids.push(String(asArray(row)[0]));
|
|
1594
|
+
scanned += asNumber(reply[2]);
|
|
1595
|
+
total = asNumber(reply[3]);
|
|
1596
|
+
const next = asNumber(reply[1], -1);
|
|
1597
|
+
if (next < 0) return { ids, scanned, total, end: at + asNumber(reply[2]), exhausted: true };
|
|
1598
|
+
at = next;
|
|
1599
|
+
}
|
|
1600
|
+
return { ids, scanned, total, end: at, exhausted: false };
|
|
1601
|
+
}
|
|
1553
1602
|
async getJob(queueName, jobId) {
|
|
1554
1603
|
const c = await this.ensureConnected();
|
|
1555
1604
|
const p = queueKeyPrefix(this.config.prefix, queueName);
|
|
@@ -2071,6 +2120,30 @@ var RedisInspector = class {
|
|
|
2071
2120
|
}
|
|
2072
2121
|
return { action, ok, failed, requested: ids.length };
|
|
2073
2122
|
}
|
|
2123
|
+
async promoteMatching(queueName, match, opts) {
|
|
2124
|
+
const query = match.query?.trim() ?? "";
|
|
2125
|
+
const groupId = match.groupId ?? "";
|
|
2126
|
+
if (!query && !groupId) throw new Error("query_or_group_required");
|
|
2127
|
+
const cursor = Math.max(0, toInt(opts.cursor ?? "0", 0));
|
|
2128
|
+
const scan = await this.scanDelayedIds(queueName, { query, groupId }, cursor, Math.max(1, opts.limit));
|
|
2129
|
+
let promoted = 0;
|
|
2130
|
+
const failed = [];
|
|
2131
|
+
let failedCount = 0;
|
|
2132
|
+
const run = async (jobId) => {
|
|
2133
|
+
try {
|
|
2134
|
+
await this.promoteJob(queueName, jobId);
|
|
2135
|
+
promoted++;
|
|
2136
|
+
} catch (err) {
|
|
2137
|
+
failedCount++;
|
|
2138
|
+
if (failed.length < 20) failed.push({ jobId, reason: errorMessage(err) });
|
|
2139
|
+
}
|
|
2140
|
+
};
|
|
2141
|
+
for (let i = 0; i < scan.ids.length; i += BULK_CONCURRENCY) {
|
|
2142
|
+
await Promise.all(scan.ids.slice(i, i + BULK_CONCURRENCY).map(run));
|
|
2143
|
+
}
|
|
2144
|
+
const next = scan.exhausted ? null : String(Math.max(0, scan.end - promoted));
|
|
2145
|
+
return { matched: scan.ids.length, promoted, failed, failedCount, scanned: scan.scanned, total: scan.total, nextCursor: next };
|
|
2146
|
+
}
|
|
2074
2147
|
/**
|
|
2075
2148
|
* Operator "discard": move an active job to failed with no retry. An
|
|
2076
2149
|
* UnrecoverableError is what bullmq's own moveToFailed treats as "do not
|
|
@@ -2154,7 +2227,7 @@ var RedisInspector = class {
|
|
|
2154
2227
|
await queue.deleteGroup(groupId);
|
|
2155
2228
|
}
|
|
2156
2229
|
};
|
|
2157
|
-
function parseStats(reply, withMetrics, windowMinutes) {
|
|
2230
|
+
function parseStats(reply, withMetrics, windowMinutes, withGroupWaiting = false) {
|
|
2158
2231
|
const r = asArray(reply);
|
|
2159
2232
|
const counts = { ...EMPTY_COUNTS };
|
|
2160
2233
|
STATE_ORDER.forEach((state, i) => {
|
|
@@ -2210,6 +2283,9 @@ function parseStats(reply, withMetrics, windowMinutes) {
|
|
|
2210
2283
|
if (withMetrics) {
|
|
2211
2284
|
stats.metrics = { completed: metricsCompleted, failed: metricsFailed };
|
|
2212
2285
|
}
|
|
2286
|
+
if (withGroupWaiting && stats.groupsCount > 0) {
|
|
2287
|
+
stats.groupWaiting = { jobs: asNumber(r[21]), groups: asNumber(r[22]) };
|
|
2288
|
+
}
|
|
2213
2289
|
return stats;
|
|
2214
2290
|
}
|
|
2215
2291
|
function parseWindowMetrics(reply, request, collectedAt) {
|
|
@@ -3570,6 +3646,42 @@ var PgInspector = class {
|
|
|
3570
3646
|
}
|
|
3571
3647
|
return { action, ok, failed, requested: ids.length };
|
|
3572
3648
|
}
|
|
3649
|
+
/** See Inspector.promoteMatching. No groups on Postgres, so a group matches nothing. */
|
|
3650
|
+
async promoteMatching(queueName, match, opts) {
|
|
3651
|
+
const query = match.query?.trim() ?? "";
|
|
3652
|
+
if (match.groupId !== void 0) return { matched: 0, promoted: 0, failed: [], failedCount: 0, scanned: 0, total: 0, nextCursor: null };
|
|
3653
|
+
if (!query) throw new Error("query_or_group_required");
|
|
3654
|
+
const limit = Math.max(1, opts.limit);
|
|
3655
|
+
const ids = [];
|
|
3656
|
+
let cursor = opts.cursor ?? "0";
|
|
3657
|
+
let scanned = 0;
|
|
3658
|
+
let total = 0;
|
|
3659
|
+
let end = Math.max(0, Math.trunc(Number(cursor)) || 0);
|
|
3660
|
+
for (let call = 0; call < 50 && cursor !== null && ids.length < limit; call++) {
|
|
3661
|
+
const page = await this.searchJobs(queueName, "delayed", query, { cursor, limit: limit - ids.length });
|
|
3662
|
+
ids.push(...page.jobs.map((j) => j.id));
|
|
3663
|
+
scanned += page.scanned;
|
|
3664
|
+
total = page.total;
|
|
3665
|
+
end += page.scanned;
|
|
3666
|
+
cursor = page.nextCursor;
|
|
3667
|
+
}
|
|
3668
|
+
let promoted = 0;
|
|
3669
|
+
let failedCount = 0;
|
|
3670
|
+
const failed = [];
|
|
3671
|
+
const run = async (jobId) => {
|
|
3672
|
+
try {
|
|
3673
|
+
await this.promoteJob(queueName, jobId);
|
|
3674
|
+
promoted++;
|
|
3675
|
+
} catch (err) {
|
|
3676
|
+
failedCount++;
|
|
3677
|
+
if (failed.length < 20) failed.push({ jobId, reason: errorMessage2(err) });
|
|
3678
|
+
}
|
|
3679
|
+
};
|
|
3680
|
+
for (let i = 0; i < ids.length; i += BULK_CONCURRENCY2) {
|
|
3681
|
+
await Promise.all(ids.slice(i, i + BULK_CONCURRENCY2).map(run));
|
|
3682
|
+
}
|
|
3683
|
+
return { matched: ids.length, promoted, failed, failedCount, scanned, total, nextCursor: cursor === null ? null : String(Math.max(0, end - promoted)) };
|
|
3684
|
+
}
|
|
3573
3685
|
/** Operator "discard": an active job straight to failed, no retry (see RedisInspector.discardJob). */
|
|
3574
3686
|
async discardJob(queueName, jobId) {
|
|
3575
3687
|
const job = await this.getBullJob(queueName, jobId);
|
|
@@ -5437,6 +5549,7 @@ var ROUTE_ACTIONS = {
|
|
|
5437
5549
|
"POST /api/connections/:id/queues/:queue/jobs/bulk/retry": "job.bulk_retry",
|
|
5438
5550
|
"POST /api/connections/:id/queues/:queue/jobs/bulk/remove": "job.bulk_remove",
|
|
5439
5551
|
"POST /api/connections/:id/queues/:queue/jobs/bulk/promote": "job.bulk_promote",
|
|
5552
|
+
"POST /api/connections/:id/queues/:queue/jobs/promote-matching": "job.promote_matching",
|
|
5440
5553
|
// queues
|
|
5441
5554
|
"POST /api/connections/:id/queues/:queue/pause": "queue.pause",
|
|
5442
5555
|
"POST /api/connections/:id/queues/:queue/resume": "queue.resume",
|
|
@@ -6708,6 +6821,26 @@ async function jobRoutes(app) {
|
|
|
6708
6821
|
return result;
|
|
6709
6822
|
});
|
|
6710
6823
|
}
|
|
6824
|
+
app.post(`${base}/promote-matching`, { preHandler: [operator] }, async (request) => {
|
|
6825
|
+
const input = promoteMatchingSchema.parse(request.body ?? {});
|
|
6826
|
+
const inspector = await app.ctx.connections.getInspector(request.params.id);
|
|
6827
|
+
const result = await withRedis(
|
|
6828
|
+
() => inspector.promoteMatching(request.params.queue, { query: input.query, groupId: input.groupId }, { cursor: input.cursor ?? null, limit: PROMOTE_MATCHING_LIMIT })
|
|
6829
|
+
);
|
|
6830
|
+
request.auditDetail({
|
|
6831
|
+
...input.groupId ? { groupId: input.groupId } : {},
|
|
6832
|
+
...input.query?.trim() ? { query: input.query.trim().slice(0, 100) } : {},
|
|
6833
|
+
matched: result.matched,
|
|
6834
|
+
promoted: result.promoted,
|
|
6835
|
+
failed: result.failedCount,
|
|
6836
|
+
...result.failed.length > 0 ? { reasons: result.failed.slice(0, 10).map((f) => `${f.jobId}: ${f.reason}`) } : {}
|
|
6837
|
+
});
|
|
6838
|
+
request.log.info(
|
|
6839
|
+
{ queue: request.params.queue, groupId: input.groupId, matched: result.matched, promoted: result.promoted, failed: result.failedCount, by: request.user?.id },
|
|
6840
|
+
"promoted matching delayed jobs"
|
|
6841
|
+
);
|
|
6842
|
+
return result;
|
|
6843
|
+
});
|
|
6711
6844
|
}
|
|
6712
6845
|
|
|
6713
6846
|
// apps/server/src/routes/groups.ts
|
|
@@ -6809,7 +6942,7 @@ async function queueRoutes(app) {
|
|
|
6809
6942
|
const { id: id3, queue } = request.params;
|
|
6810
6943
|
const row = await app.ctx.connections.getRow(id3);
|
|
6811
6944
|
const inspector = app.ctx.connections.inspectorFor(row);
|
|
6812
|
-
const stats = await withRedis(() => inspector.getQueueStats([queue], { withMetrics: true }));
|
|
6945
|
+
const stats = await withRedis(() => inspector.getQueueStats([queue], { withMetrics: true, groupWaitingCap: GROUP_WAITING_CAP }));
|
|
6813
6946
|
const s = stats[queue];
|
|
6814
6947
|
if (!s) throw notFound("Queue");
|
|
6815
6948
|
const summary = {
|
|
@@ -6824,6 +6957,7 @@ async function queueRoutes(app) {
|
|
|
6824
6957
|
rates: s.rates
|
|
6825
6958
|
};
|
|
6826
6959
|
if (s.metrics) summary.metrics = s.metrics;
|
|
6960
|
+
if (s.groupWaiting) summary.groupWaiting = { jobs: s.groupWaiting.jobs, complete: s.groupWaiting.groups >= s.groupsCount };
|
|
6827
6961
|
return summary;
|
|
6828
6962
|
});
|
|
6829
6963
|
app.get(`${base}/setup`, { preHandler: [viewer] }, async (request) => {
|
|
@@ -9551,6 +9685,19 @@ var MCP_TOOLS = [
|
|
|
9551
9685
|
schema: zQueue,
|
|
9552
9686
|
run: (a, ctx) => forward(ctx, { method: "POST", url: `${queuePath(a)}/resume` })
|
|
9553
9687
|
}),
|
|
9688
|
+
tool({
|
|
9689
|
+
name: "promote_matching",
|
|
9690
|
+
title: "Promote every matching delayed job",
|
|
9691
|
+
description: "Promotes the delayed jobs of a BullMQ Pro group (group_id) and / or whose id, name or data contains query, beyond the 500-id limit of bulk_job_action. Each call promotes at most 2000 and returns next_cursor: call again with it until it is null. Grouped jobs go back into their group (needs BullMQ Pro's package next to Bullpane).",
|
|
9692
|
+
access: "write",
|
|
9693
|
+
inputSchema: object(
|
|
9694
|
+
{ ...QUEUE, query: str4("Text the job id, name or data must contain"), group_id: str4("BullMQ Pro group id (exact)"), cursor: str4("next_cursor of the previous call") },
|
|
9695
|
+
["connection_id", "queue"]
|
|
9696
|
+
),
|
|
9697
|
+
annotations: rw(false, false),
|
|
9698
|
+
schema: zQueue.extend({ query: z10.string().max(500).optional(), group_id: z10.string().min(1).max(200).optional(), cursor: z10.string().max(20).optional() }).refine((a) => !!a.query?.trim() || !!a.group_id, { message: "query or group_id is required", path: ["query"] }),
|
|
9699
|
+
run: (a, ctx) => forward(ctx, { method: "POST", url: `${queuePath(a)}/jobs/promote-matching`, body: { query: a.query, groupId: a.group_id, cursor: a.cursor } })
|
|
9700
|
+
}),
|
|
9554
9701
|
tool({
|
|
9555
9702
|
name: "pause_group",
|
|
9556
9703
|
title: "Pause a group (BullMQ Pro)",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bullpane",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Self-hosted dashboard for BullMQ and BullMQ Pro, on Redis or on BullMQ 6's Postgres backend: queues, jobs, failures, metrics, server health, flows and Pro groups. A bull-board alternative you can run with npx.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"bullmq",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as ae,u as $,a as Z,j as e,L as G,P as _,S as J,E as A,W as O,N as ne,r as N,b as oe,d as re,e as le,f as c,g as ie,h as de,i as ce,k as S,l as v,B as b,R as ue,m as me,n as F,I as xe,o as R,p as K,q as U,t as E,T as ge,s as P,v as X,D as he,w as pe,x as fe}from"./index-
|
|
1
|
+
import{c as ae,u as $,a as Z,j as e,L as G,P as _,S as J,E as A,W as O,N as ne,r as N,b as oe,d as re,e as le,f as c,g as ie,h as de,i as ce,k as S,l as v,B as b,R as ue,m as me,n as F,I as xe,o as R,p as K,q as U,t as E,T as ge,s as P,v as X,D as he,w as pe,x as fe}from"./index-DS8k2gIv.js";import{u as be,a as je,l as H,i as ve,B as Ne,b as we,C as ye,M as ke,c as Ce,H as V,P as Q}from"./flowLayout-Bwc9MVRI.js";/**
|
|
2
2
|
* @license lucide-react v0.469.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{c as T,b as B,y as L,J as P,j as e,P as g,K as b,S as $,E as f,p as x,r as u,n as z,M as w,l as p,C as F,f as h,O as j,v as N,o as K,Q as O,U as S,V as y}from"./index-
|
|
1
|
+
import{c as T,b as B,y as L,J as P,j as e,P as g,K as b,S as $,E as f,p as x,r as u,n as z,M as w,l as p,C as F,f as h,O as j,v as N,o as K,Q as O,U as S,V as y}from"./index-DS8k2gIv.js";import{u as A,a as H,i as q,B as I,b as J,C as V,M as G,l as R,c as D,H as k,P as v}from"./flowLayout-Bwc9MVRI.js";/**
|
|
2
2
|
* @license lucide-react v0.469.0 - ISC
|
|
3
3
|
*
|
|
4
4
|
* This source code is licensed under the ISC license.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{y as S,z as P,e as N,j as e,A as o,C as M,S as C,N as A,F as B,G as q,f as p,n as j,B as m,H as E,v as z}from"./index-
|
|
1
|
+
import{y as S,z as P,e as N,j as e,A as o,C as M,S as C,N as A,F as B,G as q,f as p,n as j,B as m,H as E,v as z}from"./index-DS8k2gIv.js";function L(){const[n]=S(),t=P(),{user:s,loading:a}=N(),r=n.get("error"),i=n.get("request");return r||!i?e.jsx(o,{title:"Could not connect to Bullpane",children:e.jsxs("p",{className:"flex gap-2 text-sm text-fg",children:[e.jsx(M,{className:"mt-0.5 size-4 shrink-0 text-danger"}),r??"This page needs a sign-in request from an MCP client. Start the connection again from Claude."]})}):a?e.jsx("div",{className:"flex min-h-screen items-center justify-center",children:e.jsx(C,{})}):s?e.jsx(D,{request:i}):e.jsx(A,{to:"/login",replace:!0,state:{from:t}})}function D({request:n}){const{user:t}=N(),s=B(n),a=q(),[r,i]=p.useState(null),[f,g]=p.useState(null);if(s.isLoading)return e.jsx("div",{className:"flex min-h-screen items-center justify-center",children:e.jsx(C,{})});if(s.isError||!s.data)return e.jsx(o,{title:"Could not connect to Bullpane",children:e.jsx("p",{className:"text-sm text-fg",children:j(s.error,"This sign-in request is no longer valid. Start the connection again from Claude.")})});const{clientName:c,redirectHost:w,allowed:l,requested:y,maxAccess:k}=s.data,d=r??(l==="write"&&y==="write"?"write":"read"),h=l==="write"?null:k!=="write"?"an admin limited MCP to read only":`your role is ${t==null?void 0:t.role}`,x=v=>{g(null),a.mutate({request:n,approve:v,access:d},{onSuccess:({redirectTo:u})=>window.location.assign(u),onError:u=>g(j(u))})};return l==="off"?e.jsxs(o,{title:"MCP is turned off",children:[e.jsxs("p",{className:"text-sm text-fg-muted",children:["An admin has to allow MCP clients in ",e.jsx("span",{className:"text-fg",children:"Settings → MCP"})," before ",c," can connect."]}),e.jsxs(m,{className:"mt-5 w-full",variant:"secondary",loading:a.isPending,onClick:()=>x(!1),children:["Back to ",c]})]}):e.jsxs(o,{title:`Connect ${c} to Bullpane`,description:e.jsxs(e.Fragment,{children:["It will act as ",e.jsx("span",{className:"text-fg",children:t==null?void 0:t.email})," (",t==null?void 0:t.role,") and send you back to ",w,"."]}),children:[e.jsxs("div",{className:"flex flex-col gap-2",role:"radiogroup","aria-label":"Access",children:[e.jsx(b,{selected:d==="read",onSelect:()=>i("read"),title:"Read only",detail:"See connections, queues, jobs, logs and schedulers."}),e.jsx(b,{selected:d==="write",disabled:l!=="write",onSelect:()=>i("write"),title:"Read & write",detail:h?`Not available: ${h}.`:"Also retry, promote, remove and add jobs, pause and resume queues."})]}),e.jsxs("p",{className:"mt-3 flex gap-1.5 text-[11px] text-fg-subtle",children:[e.jsx(E,{className:"size-3.5 shrink-0"}),"Every change is in the audit log. Drain, clean and obliterate always need you in the dashboard. Disconnect any time in Settings → MCP."]}),f&&e.jsx("p",{className:"mt-3 text-xs text-danger",children:f}),e.jsxs("div",{className:"mt-5 flex gap-2",children:[e.jsx(m,{className:"flex-1",variant:"ghost",disabled:a.isPending,onClick:()=>x(!1),children:"Cancel"}),e.jsx(m,{className:"flex-1",variant:"primary",loading:a.isPending,onClick:()=>x(!0),children:"Allow"})]})]})}function b({selected:n,disabled:t,onSelect:s,title:a,detail:r}){return e.jsxs("button",{type:"button",role:"radio","aria-checked":n,disabled:t,onClick:s,className:z("rounded-md border px-3 py-2.5 text-left transition-colors disabled:cursor-not-allowed disabled:opacity-60",n?"border-accent bg-accent/10":"border-border bg-surface-2 hover:border-border-strong"),children:[e.jsx("span",{className:"block text-sm font-medium text-fg",children:a}),e.jsx("span",{className:"mt-0.5 block text-xs text-fg-muted",children:r})]})}export{L as McpConsentPage};
|