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.
@@ -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.2",
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-CN2nADIL.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-GUFMVhrV.js";/**
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-CN2nADIL.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-GUFMVhrV.js";/**
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-CN2nADIL.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};
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};