bunqueue-dashboard 0.0.37 → 0.0.39

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.
Files changed (109) hide show
  1. package/README.md +21 -11
  2. package/agent/backup/resultValidation.ts +1 -1
  3. package/agent/backup/routes.ts +63 -17
  4. package/agent/db/query.ts +2 -1
  5. package/agent/manager/config.ts +2 -1
  6. package/agent/manager/process.ts +51 -5
  7. package/agent/manager/storageMode.ts +21 -0
  8. package/agent/manager/types.ts +2 -0
  9. package/agent/manager.ts +1 -0
  10. package/agent/queue/routes.ts +1 -2
  11. package/agent/queue/runtime.ts +5 -0
  12. package/agent/queue/types.ts +1 -0
  13. package/agent/server/controlRoutes.ts +109 -8
  14. package/agent/server/databaseRoutes.ts +47 -14
  15. package/agent/server/errors.ts +1 -0
  16. package/agent/server/lifecycle.ts +25 -8
  17. package/agent/server/managedRuntime.ts +27 -0
  18. package/agent/server/router.ts +90 -40
  19. package/agent/server/workflowReadRoutes.ts +28 -11
  20. package/agent/workflows.ts +1 -1
  21. package/dist/.vite/manifest.json +490 -476
  22. package/dist/assets/AddJob-d5KBvmN8.js +3 -0
  23. package/dist/assets/{Alerts-Gh1pVbqj.js → Alerts-5I44QTnQ.js} +1 -1
  24. package/dist/assets/{AreaChart-w0ruozXH.js → AreaChart-DEAalJ6P.js} +1 -1
  25. package/dist/assets/Benchmark-S1afsQRd.js +1 -0
  26. package/dist/assets/{BulkAddJobs-Di_IgRSH.js → BulkAddJobs-BO8LWUHE.js} +1 -1
  27. package/dist/assets/{Button-BPqpRZJD.js → Button-Ch1W94fJ.js} +1 -1
  28. package/dist/assets/{Card-C7NMhqUt.js → Card-CWxrC8OY.js} +1 -1
  29. package/dist/assets/{CopilotPanel-Db5nYpo1.js → CopilotPanel-DDkRd35p.js} +4 -4
  30. package/dist/assets/{Cron-BRgWcBGk.js → Cron-X3Y5uISO.js} +1 -1
  31. package/dist/assets/CronManager-ugmYEAwY.js +1 -0
  32. package/dist/assets/{Database-BeoEreW0.js → Database-koy8haPX.js} +1 -1
  33. package/dist/assets/Diagnostics-D6oqvkOs.js +1 -0
  34. package/dist/assets/{Dlq-DfCg04Jd.js → Dlq-D70Lzmsa.js} +1 -1
  35. package/dist/assets/{DlqControl-BRCkUXvb.js → DlqControl-nUT5Zzkl.js} +1 -1
  36. package/dist/assets/{DlqPro-BHJwBYsN.js → DlqPro-VMuBKgP-.js} +1 -1
  37. package/dist/assets/Flows-CAG0nDnt.js +2 -0
  38. package/dist/assets/JobInspector-CiS5HOSQ.js +2 -0
  39. package/dist/assets/{Jobs-URpnFjC6.js → Jobs-CkyRvzOT.js} +1 -1
  40. package/dist/assets/{JobsPro-pQ_RFSU3.js → JobsPro-B4-CSoq3.js} +1 -1
  41. package/dist/assets/{Logs-C5jX2OPE.js → Logs-DBTz2PTz.js} +1 -1
  42. package/dist/assets/{LogsPro-Cq_npmmS.js → LogsPro-BCFeegvr.js} +1 -1
  43. package/dist/assets/{McpServer-DtCTmQBu.js → McpServer-BKlTR1qr.js} +2 -2
  44. package/dist/assets/{Metrics-BdxitePr.js → Metrics-Du__9Lbn.js} +1 -1
  45. package/dist/assets/{MetricsPro-orHfW0mg.js → MetricsPro-CqnaO6YX.js} +1 -1
  46. package/dist/assets/{Overview-DaNns1aj.js → Overview-udSSt7ZB.js} +1 -1
  47. package/dist/assets/{PageHeader-BXlZSBcc.js → PageHeader-CvIyaFOO.js} +1 -1
  48. package/dist/assets/{Pagination-Q-vf4hsX.js → Pagination-x-Cwgrul.js} +1 -1
  49. package/dist/assets/QueueActions-D7cQrGvJ.js +1 -0
  50. package/dist/assets/{QueueControl-l5bSdMgb.js → QueueControl-BF638r_0.js} +1 -1
  51. package/dist/assets/QueueDetail-C-ms6bMe.js +1 -0
  52. package/dist/assets/QueueDetailPro-DXEdsZx_.js +1 -0
  53. package/dist/assets/{Queues-DeLXlww2.js → Queues-BUMwTVAf.js} +1 -1
  54. package/dist/assets/{QueuesOverview-DoQ-MUfT.js → QueuesOverview-Du8XLgN0.js} +1 -1
  55. package/dist/assets/{S3Backup-D2agV9_z.js → S3Backup-CDICSzxM.js} +1 -1
  56. package/dist/assets/S3BackupPro-CBidbI-s.js +3 -0
  57. package/dist/assets/ServerControl-CPT0LpsC.js +3 -0
  58. package/dist/assets/{Settings-CiJRj_SM.js → Settings-CmxfSX_8.js} +1 -1
  59. package/dist/assets/{StatusBadge-B0I7hNBX.js → StatusBadge-BoOVTnKu.js} +1 -1
  60. package/dist/assets/{Usage-DbHJgA4t.js → Usage-B895tSxi.js} +1 -1
  61. package/dist/assets/{UsagePro-LoPo3k1L.js → UsagePro-CASN77iV.js} +1 -1
  62. package/dist/assets/{Webhooks-CuzIJHwk.js → Webhooks-CXbZOFFw.js} +1 -1
  63. package/dist/assets/{Workers-BpUgpwUH.js → Workers-BceQlUhX.js} +1 -1
  64. package/dist/assets/WorkersPro-Dtml89jB.js +1 -0
  65. package/dist/assets/Workflows-B8wmQN2X.js +1 -0
  66. package/dist/assets/{api-V_sVpHDo.js → api-81m1TwB7.js} +1 -1
  67. package/dist/assets/bq-BOfwNIlJ.js +1 -0
  68. package/dist/assets/{connectionStore-CKFQrxDo.js → connectionStore-CFuNYZ_8.js} +1 -1
  69. package/dist/assets/{controlConnectionEpoch-JM4omZ6j.js → controlConnectionEpoch-DzbuWOhe.js} +1 -1
  70. package/dist/assets/{feedback-BnGE2oW2.js → feedback-QDN1AsYt.js} +1 -1
  71. package/dist/assets/flowMutationSafety-6cP-l4Pa.js +1 -0
  72. package/dist/assets/{form-tC20r4Ma.js → form-C4oby9pI.js} +1 -1
  73. package/dist/assets/{icons-CIvFfU2b.js → icons-BVzwsfbG.js} +1 -1
  74. package/dist/assets/index-B-QjN7pA.css +1 -0
  75. package/dist/assets/index-TploGF7u.js +8 -0
  76. package/dist/assets/install-BPp8nydm.js +3 -0
  77. package/dist/assets/{react-vendor-CyjgmGTq.js → react-vendor-CvwrP0tU.js} +2 -2
  78. package/dist/assets/{responses-CBLyy1Ob.js → responses-C9lP49qu.js} +1 -1
  79. package/dist/assets/{toastStore-B7n0zS0y.js → toastStore-B31o6Z5J.js} +1 -1
  80. package/dist/assets/{upstreamPaths-CDVsu6IM.js → upstreamPaths-ZeijhPPb.js} +1 -1
  81. package/dist/assets/useControlActionGuard-DT7uudrj.js +1 -0
  82. package/dist/assets/{usePolledData-c53tx1jw.js → usePolledData-CNtNtd0a.js} +1 -1
  83. package/dist/assets/useServerActionGuard-Db7PPgaI.js +1 -0
  84. package/dist/assets/useThroughputSeries-_J7gWbcI.js +1 -0
  85. package/dist/index.html +21 -18
  86. package/package.json +18 -8
  87. package/scripts/embedded.gen.ts +152 -150
  88. package/scripts/serveHandler.ts +22 -1
  89. package/scripts/servePolicy.ts +12 -3
  90. package/dist/assets/AddJob-CWkkVHmf.js +0 -3
  91. package/dist/assets/Benchmark-Dir2A1CQ.js +0 -1
  92. package/dist/assets/CronManager-CbM0x_s0.js +0 -1
  93. package/dist/assets/Diagnostics-Cw9iJ84f.js +0 -1
  94. package/dist/assets/Flows-BAzbFP_W.js +0 -2
  95. package/dist/assets/JobInspector-qvW_APO6.js +0 -2
  96. package/dist/assets/QueueActions-jiTzndPH.js +0 -1
  97. package/dist/assets/QueueDetail-URK2pjtH.js +0 -1
  98. package/dist/assets/QueueDetailPro-C9M5CV4K.js +0 -1
  99. package/dist/assets/S3BackupPro--WSZWTQn.js +0 -3
  100. package/dist/assets/ServerControl-QVd6m_iR.js +0 -3
  101. package/dist/assets/WorkersPro-BYY0P1Pt.js +0 -1
  102. package/dist/assets/Workflows-ByJw4FFH.js +0 -1
  103. package/dist/assets/bq-DYmbVkgR.js +0 -1
  104. package/dist/assets/flowMutationSafety-DObp9nJo.js +0 -1
  105. package/dist/assets/index-D64v2vNO.js +0 -8
  106. package/dist/assets/index-DOze4TKg.css +0 -1
  107. package/dist/assets/install-jXi1SOYY.js +0 -3
  108. package/dist/assets/useServerActionGuard-OTv6_fU0.js +0 -1
  109. package/dist/assets/useThroughputSeries-DWHQl7cM.js +0 -1
@@ -10,6 +10,7 @@ import {
10
10
  } from '../db';
11
11
  import { corsHeaders } from './policy';
12
12
  import { readLimitedJsonBody } from './jsonBody';
13
+ import type { ManagedDatabaseAdmission } from './managedRuntime';
13
14
  import type { RouteResponse } from './types';
14
15
 
15
16
  const MAX_QUERY_BODY_BYTES = 64 * 1024;
@@ -100,46 +101,58 @@ export async function routeDatabaseRequest(
100
101
  method: string,
101
102
  dataPath: string,
102
103
  origin: string | null,
103
- allowedOrigins: string[]
104
+ allowedOrigins: string[],
105
+ admission?: ManagedDatabaseAdmission
104
106
  ): Promise<RouteResponse | Response | null> {
105
107
  if (pathname === '/db/info' && method === 'GET') {
106
- return { status: 200, body: { ok: true, ...dbInfo(dataPath) } };
108
+ return withDatabase(dataPath, admission, (path) => ({
109
+ status: 200,
110
+ body: { ok: true, ...dbInfo(path) },
111
+ }));
107
112
  }
108
113
  if (pathname === '/db/tables' && method === 'GET') {
109
- return { status: 200, body: { ok: true, tables: dbTables(dataPath) } };
114
+ return withDatabase(dataPath, admission, (path) => ({
115
+ status: 200,
116
+ body: { ok: true, tables: dbTables(path) },
117
+ }));
110
118
  }
111
119
  if (pathname.startsWith('/db/tables/') && method === 'GET') {
112
120
  const segments = pathname.slice('/db/tables/'.length).split('/');
113
121
  const table = decodeURIComponent(segments[0] ?? '');
114
122
  const subresource = segments[1];
115
123
  if (segments.length === 2 && subresource === 'export') {
116
- return exportTable(request, table, dataPath, origin, allowedOrigins);
124
+ return withDatabase(dataPath, admission, (path) =>
125
+ exportTable(request, table, path, origin, allowedOrigins)
126
+ );
117
127
  }
118
128
  if (segments.length === 2 && subresource === 'schema') {
119
- return { status: 200, body: { ok: true, ...dbSchema(dataPath, table) } };
129
+ return withDatabase(dataPath, admission, (path) => ({
130
+ status: 200,
131
+ body: { ok: true, ...dbSchema(path, table) },
132
+ }));
120
133
  }
121
134
  if (segments.length === 2 && subresource === 'cell') {
122
135
  const query = new URL(request.url).searchParams;
123
136
  const rowid = query.get('rowid');
124
137
  if (rowid === null) throw new Error('rowid is required');
125
- return {
138
+ return withDatabase(dataPath, admission, (path) => ({
126
139
  status: 200,
127
140
  body: {
128
141
  ok: true,
129
- ...dbCell(dataPath, table, rowid, query.get('column') ?? ''),
142
+ ...dbCell(path, table, rowid, query.get('column') ?? ''),
130
143
  },
131
- };
144
+ }));
132
145
  }
133
146
  if (segments.length !== 1) {
134
147
  return { status: 404, body: { ok: false, error: 'Not found' } };
135
148
  }
136
149
  const query = new URL(request.url).searchParams;
137
- return {
150
+ return withDatabase(dataPath, admission, (path) => ({
138
151
  status: 200,
139
152
  body: {
140
153
  ok: true,
141
154
  ...dbRows(
142
- dataPath,
155
+ path,
143
156
  table,
144
157
  Number(query.get('limit')) || 50,
145
158
  Number(query.get('offset')) || 0,
@@ -148,7 +161,7 @@ export async function routeDatabaseRequest(
148
161
  browseFilter(query)
149
162
  ),
150
163
  },
151
- };
164
+ }));
152
165
  }
153
166
  if (pathname === '/db/query' && method === 'POST') {
154
167
  const { sql } = (await readLimitedJsonBody(request, {
@@ -156,10 +169,30 @@ export async function routeDatabaseRequest(
156
169
  maxBytes: MAX_QUERY_BODY_BYTES,
157
170
  limitLabel: '64 KiB',
158
171
  })) as { sql?: string };
159
- return {
172
+ return withDatabase(dataPath, admission, async (path) => ({
160
173
  status: 200,
161
- body: { ok: true, ...(await queryWithTimeout(dataPath, sql ?? '')) },
162
- };
174
+ body: { ok: true, ...(await queryWithTimeout(path, sql ?? '')) },
175
+ }));
163
176
  }
164
177
  return null;
165
178
  }
179
+
180
+ function withDatabase<T>(
181
+ dataPath: string,
182
+ admission: ManagedDatabaseAdmission | undefined,
183
+ operation: (path: string) => T | Promise<T>
184
+ ): Promise<T | RouteResponse> {
185
+ return admission
186
+ ? admission((snapshot) =>
187
+ snapshot.storageMode === 'sqlite'
188
+ ? operation(snapshot.dataPath)
189
+ : {
190
+ status: 409,
191
+ body: {
192
+ ok: false,
193
+ error: `Database inspection requires Bunqueue SQLite storage; ${snapshot.storageMode} is active`,
194
+ },
195
+ }
196
+ )
197
+ : Promise.resolve(operation(dataPath));
198
+ }
@@ -19,6 +19,7 @@ const CONFLICT_MESSAGES = [
19
19
  'BUNQUEUE_WORKFLOW_CONCURRENCY must',
20
20
  'Managed Bunqueue server restarted',
21
21
  'Server lifecycle is disabled in external mode',
22
+ 'Configuration changed since revision',
22
23
  ];
23
24
 
24
25
  export function errorStatus(error: unknown): number {
@@ -18,6 +18,7 @@ export class AgentLifecycleGate implements AgentLifecyclePort {
18
18
  private leases = new Set<Promise<void>>();
19
19
  private closed = false;
20
20
  private closing: Promise<void> | null = null;
21
+ private pendingTransitions = 0;
21
22
 
22
23
  assertOpen(): void {
23
24
  if (this.closed) throw new AgentLifecycleClosedError();
@@ -29,14 +30,25 @@ export class AgentLifecycleGate implements AgentLifecyclePort {
29
30
  } catch (error) {
30
31
  return Promise.reject(error);
31
32
  }
32
- const result = this.barrier.then(operation);
33
33
  const batch = this.leases;
34
- const completion = result.then(
35
- () => undefined,
36
- () => undefined
37
- );
34
+ let complete!: () => void;
35
+ const completion = new Promise<void>((resolve) => {
36
+ complete = resolve;
37
+ });
38
+ // Publish the lease before invoking user code. Its synchronous prefix may
39
+ // re-enter run()/close(), which must observe this lease and wait for it.
38
40
  batch.add(completion);
39
- void completion.then(() => batch.delete(completion));
41
+ let result: Promise<T>;
42
+ if (this.pendingTransitions === 0) {
43
+ try {
44
+ result = Promise.resolve(operation());
45
+ } catch (error) {
46
+ result = Promise.reject(error);
47
+ }
48
+ } else {
49
+ result = this.barrier.then(operation);
50
+ }
51
+ void result.then(complete, complete).then(() => batch.delete(completion));
40
52
  return result;
41
53
  }
42
54
 
@@ -60,12 +72,17 @@ export class AgentLifecycleGate implements AgentLifecyclePort {
60
72
  const precedingBarrier = this.barrier;
61
73
  const precedingLeases = this.leases;
62
74
  this.leases = new Set();
75
+ this.pendingTransitions += 1;
63
76
  const result = precedingBarrier
64
77
  .then(() => Promise.all(precedingLeases))
65
78
  .then(operation);
66
79
  this.barrier = result.then(
67
- () => undefined,
68
- () => undefined
80
+ () => {
81
+ this.pendingTransitions -= 1;
82
+ },
83
+ () => {
84
+ this.pendingTransitions -= 1;
85
+ }
69
86
  );
70
87
  return result;
71
88
  }
@@ -1,4 +1,6 @@
1
1
  import type { ProcessManager, ServerConfig, StatusSnapshot } from '../manager';
2
+ import { managedStorageMode } from '../manager';
3
+ import type { ManagedStorageMode } from '../manager/storageMode';
2
4
  import type { AgentLifecyclePort } from './lifecycle';
3
5
 
4
6
  export interface ManagedRuntimeSnapshot {
@@ -10,6 +12,15 @@ export type ManagedRuntimeAdmission = <T>(
10
12
  operation: (snapshot: ManagedRuntimeSnapshot) => Promise<T>
11
13
  ) => Promise<T>;
12
14
 
15
+ export interface ManagedDatabaseSnapshot {
16
+ dataPath: string;
17
+ storageMode: ManagedStorageMode;
18
+ }
19
+
20
+ export type ManagedDatabaseAdmission = <T>(
21
+ operation: (snapshot: ManagedDatabaseSnapshot) => T | Promise<T>
22
+ ) => Promise<T>;
23
+
13
24
  /**
14
25
  * Admit SDK work against one managed-process generation. The operation and its
15
26
  * final state/config check share the same lifecycle section as start/stop.
@@ -32,6 +43,22 @@ export function managedRuntimeAdmission(
32
43
  });
33
44
  }
34
45
 
46
+ /** Pin DB-backed observability to the live process config for one reader lease. */
47
+ export function managedDatabaseAdmission(
48
+ manager: ProcessManager,
49
+ lifecycle: AgentLifecyclePort
50
+ ): ManagedDatabaseAdmission {
51
+ return (operation) =>
52
+ lifecycle.lease(async () => {
53
+ const snapshot = manager.getStatus();
54
+ const config = snapshot.runningConfig ?? snapshot.config;
55
+ return operation({
56
+ dataPath: config.dataPath,
57
+ storageMode: managedStorageMode(config),
58
+ });
59
+ });
60
+ }
61
+
35
62
  function generationChanged(initial: StatusSnapshot, current: StatusSnapshot): boolean {
36
63
  return (
37
64
  initial.status === 'running' &&
@@ -1,8 +1,11 @@
1
- import { routeBackupRequest } from '../backup/routes';
1
+ import {
2
+ executePreparedBackupRequest,
3
+ prepareBackupRequest,
4
+ } from '../backup/routes';
2
5
  import type { BackupRunnerPort } from '../backup/runner';
3
6
  import { routeFlowRequest } from '../flow/routes';
4
7
  import type { ManagedTargetPolicy } from '../managedTarget';
5
- import type { ProcessManager } from '../manager';
8
+ import { managedStorageMode, type ProcessManager } from '../manager';
6
9
  import { routeQueueOperationsRequest } from '../queue/routes';
7
10
  import type { QueueOperationsPort } from '../queue/types';
8
11
  import type { WorkflowRuntimePort } from '../workflow/runtime';
@@ -11,7 +14,7 @@ import { assertManagedControl, routeControlRequest } from './controlRoutes';
11
14
  import type { ServerControlTarget } from './controlTarget';
12
15
  import { routeDatabaseRequest } from './databaseRoutes';
13
16
  import type { AgentLifecyclePort } from './lifecycle';
14
- import { managedRuntimeAdmission } from './managedRuntime';
17
+ import { managedDatabaseAdmission, managedRuntimeAdmission } from './managedRuntime';
15
18
  import type { RouteResponse } from './types';
16
19
  import { routeWorkflowReadRequest } from './workflowReadRoutes';
17
20
 
@@ -44,7 +47,9 @@ export async function routeAgentRequest(
44
47
 
45
48
  const snapshot = manager.getStatus();
46
49
  const managedConfig = snapshot.runningConfig ?? snapshot.config;
50
+ const storageMode = managedStorageMode(managedConfig);
47
51
  const admission = managedRuntimeAdmission(manager, lifecycle, snapshot);
52
+ const databaseAdmission = managedDatabaseAdmission(manager, lifecycle);
48
53
  if (pathname.startsWith('/flows/')) {
49
54
  const flow = await routeFlowRequest(
50
55
  request,
@@ -73,14 +78,16 @@ export async function routeAgentRequest(
73
78
  if (workflowRuntime) return workflowRuntime;
74
79
  }
75
80
 
76
- const desiredConfig = manager.getConfig();
77
- const workflowRead = routeWorkflowReadRequest(
78
- request,
79
- pathname,
80
- method,
81
- managedConfig.dataPath
82
- );
83
- if (workflowRead) return workflowRead;
81
+ if (isWorkflowReadRoute(pathname, method)) {
82
+ const workflowRead = await routeWorkflowReadRequest(
83
+ request,
84
+ pathname,
85
+ method,
86
+ managedConfig.dataPath,
87
+ databaseAdmission
88
+ );
89
+ if (workflowRead) return workflowRead;
90
+ }
84
91
 
85
92
  if (pathname.startsWith('/queue-operations/')) {
86
93
  const queueOperation = await routeQueueOperationsRequest(
@@ -97,54 +104,93 @@ export async function routeAgentRequest(
97
104
  }
98
105
 
99
106
  if (pathname.startsWith('/backup/')) {
107
+ if (storageMode !== 'sqlite') return sqliteOnly('S3 backup', storageMode);
100
108
  if (pathname === '/backup/restore' && method === 'POST') {
101
109
  assertManagedControl(controlTarget);
102
- return manager.withStoppedMaintenance('restoring a backup', async () => {
103
- const stoppedSnapshot = manager.getStatus();
104
- const restoreDesired = manager.getConfig();
105
- const restoreConfig = {
106
- ...(stoppedSnapshot.runningConfig ?? stoppedSnapshot.config),
107
- extraEnv: restoreDesired.extraEnv,
108
- };
109
- return routeBackupRequest(
110
- request,
111
- pathname,
112
- method,
113
- restoreConfig,
114
- stoppedSnapshot.status === 'running',
115
- await manager.dbStats(),
116
- backupRunner,
117
- undefined,
118
- managedTargetPolicy
119
- );
110
+ const prepared = await prepareBackupRequest(
111
+ request,
112
+ pathname,
113
+ method,
114
+ managedConfig,
115
+ managedTargetPolicy
116
+ );
117
+ if (!prepared || prepared.operation !== 'restore') return null;
118
+ return lifecycle.run(async () => {
119
+ const current = manager.getStatus();
120
+ const restoreConfig = current.runningConfig ?? current.config;
121
+ const currentStorageMode = managedStorageMode(restoreConfig);
122
+ if (currentStorageMode !== 'sqlite') {
123
+ return sqliteOnly('S3 backup', currentStorageMode);
124
+ }
125
+ return manager.withStoppedMaintenance('restoring a backup', async () => {
126
+ return executePreparedBackupRequest(
127
+ prepared,
128
+ restoreConfig,
129
+ current.status === 'running',
130
+ await manager.dbStats(restoreConfig.dataPath),
131
+ backupRunner,
132
+ undefined,
133
+ managedTargetPolicy
134
+ );
135
+ });
120
136
  });
121
137
  }
122
- const database = await manager.dbStats();
123
- const backupConfig = { ...managedConfig, extraEnv: desiredConfig.extraEnv };
124
- const backup = await routeBackupRequest(
138
+ const prepared = await prepareBackupRequest(
125
139
  request,
126
140
  pathname,
127
141
  method,
128
- backupConfig,
129
- snapshot.status === 'running',
130
- database,
131
- backupRunner,
132
- (extraEnv) => manager.setConfig({ extraEnv }),
142
+ managedConfig,
133
143
  managedTargetPolicy
134
144
  );
135
- if (backup) return backup;
145
+ if (prepared) {
146
+ const execute = async () => {
147
+ const current = manager.getStatus();
148
+ const desired = manager.getConfig();
149
+ const effective = current.runningConfig ?? current.config;
150
+ const backupConfig = { ...effective, extraEnv: desired.extraEnv };
151
+ const currentStorageMode = managedStorageMode(backupConfig);
152
+ if (currentStorageMode !== 'sqlite') {
153
+ return sqliteOnly('S3 backup', currentStorageMode);
154
+ }
155
+ return executePreparedBackupRequest(
156
+ prepared,
157
+ backupConfig,
158
+ current.status === 'running',
159
+ await manager.dbStats(backupConfig.dataPath),
160
+ backupRunner,
161
+ (extraEnv) => {
162
+ manager.setConfig({ extraEnv });
163
+ },
164
+ managedTargetPolicy
165
+ );
166
+ };
167
+ return prepared.operation === 'configure'
168
+ ? lifecycle.run(execute)
169
+ : lifecycle.lease(execute);
170
+ }
136
171
  }
137
172
 
173
+ if ((pathname === '/db' || pathname.startsWith('/db/')) && storageMode !== 'sqlite') {
174
+ return sqliteOnly('Database inspection', storageMode);
175
+ }
138
176
  return routeDatabaseRequest(
139
177
  request,
140
178
  pathname,
141
179
  method,
142
- desiredConfig.dataPath,
180
+ managedConfig.dataPath,
143
181
  origin,
144
- allowedOrigins
182
+ allowedOrigins,
183
+ databaseAdmission
145
184
  );
146
185
  }
147
186
 
187
+ function sqliteOnly(feature: string, storageMode: 'memory' | 'postgres'): RouteResponse {
188
+ return {
189
+ status: 409,
190
+ body: { ok: false, error: `${feature} requires Bunqueue SQLite storage; ${storageMode} is active` },
191
+ };
192
+ }
193
+
148
194
  function isWorkflowRuntimeRoute(pathname: string, method: string): boolean {
149
195
  if (method === 'GET') return pathname === '/workflows/runtime';
150
196
  if (method !== 'POST') return false;
@@ -159,3 +205,7 @@ function isWorkflowRuntimeRoute(pathname: string, method: string): boolean {
159
205
  /^\/workflows\/[^/]+\/(signal|resume-compensation|abandon-compensation)$/.test(pathname)
160
206
  );
161
207
  }
208
+
209
+ function isWorkflowReadRoute(pathname: string, method: string): boolean {
210
+ return method === 'GET' && (pathname === '/workflows' || pathname.startsWith('/workflows/'));
211
+ }
@@ -7,6 +7,7 @@ import {
7
7
  WORKFLOW_STATES,
8
8
  } from '../workflows';
9
9
  import type { RouteResponse } from './types';
10
+ import type { ManagedDatabaseAdmission } from './managedRuntime';
10
11
 
11
12
  function workflowKind(query: URLSearchParams): WorkflowStoreKind {
12
13
  const kind = query.get('kind') ?? 'active';
@@ -23,14 +24,18 @@ function integer(query: URLSearchParams, name: 'limit' | 'offset', fallback: num
23
24
  return Number(raw);
24
25
  }
25
26
 
26
- export function routeWorkflowReadRequest(
27
+ export async function routeWorkflowReadRequest(
27
28
  request: Request,
28
29
  pathname: string,
29
30
  method: string,
30
- dataPath: string
31
- ): RouteResponse | null {
31
+ dataPath: string,
32
+ admission?: ManagedDatabaseAdmission
33
+ ): Promise<RouteResponse | null> {
32
34
  if (pathname === '/workflows/stats' && method === 'GET') {
33
- return { status: 200, body: { ok: true, ...workflowStats(dataPath) } };
35
+ return withDatabase(dataPath, admission, (path) => ({
36
+ status: 200,
37
+ body: { ok: true, ...workflowStats(path) },
38
+ }));
34
39
  }
35
40
  if (pathname === '/workflows' && method === 'GET') {
36
41
  const query = new URL(request.url).searchParams;
@@ -47,11 +52,11 @@ export function routeWorkflowReadRequest(
47
52
  ) {
48
53
  throw new Error('Unknown workflow execution state');
49
54
  }
50
- return {
55
+ return withDatabase(dataPath, admission, (path) => ({
51
56
  status: 200,
52
57
  body: {
53
58
  ok: true,
54
- ...workflowExecutions(dataPath, {
59
+ ...workflowExecutions(path, {
55
60
  kind: workflowKind(query),
56
61
  workflowName: query.get('workflowName') || undefined,
57
62
  state: state as WorkflowStateFilter | undefined,
@@ -59,7 +64,7 @@ export function routeWorkflowReadRequest(
59
64
  offset: integer(query, 'offset', 0),
60
65
  }),
61
66
  },
62
- };
67
+ }));
63
68
  }
64
69
  if (!pathname.startsWith('/workflows/') || method !== 'GET') return null;
65
70
  const rawId = pathname.slice('/workflows/'.length);
@@ -73,8 +78,20 @@ export function routeWorkflowReadRequest(
73
78
  throw new Error(`Duplicate workflow detail option: ${key}`);
74
79
  }
75
80
  }
76
- const execution = workflowExecution(dataPath, decodeURIComponent(rawId), workflowKind(query));
77
- return execution
78
- ? { status: 200, body: { ok: true, execution } }
79
- : { status: 404, body: { ok: false, error: 'Workflow execution not found' } };
81
+ return withDatabase(dataPath, admission, (path) => {
82
+ const execution = workflowExecution(path, decodeURIComponent(rawId), workflowKind(query));
83
+ return execution
84
+ ? { status: 200, body: { ok: true, execution } }
85
+ : { status: 404, body: { ok: false, error: 'Workflow execution not found' } };
86
+ });
87
+ }
88
+
89
+ function withDatabase<T>(
90
+ dataPath: string,
91
+ admission: ManagedDatabaseAdmission | undefined,
92
+ operation: (path: string) => T
93
+ ): Promise<T> {
94
+ return admission
95
+ ? admission((snapshot) => operation(snapshot.dataPath))
96
+ : Promise.resolve(operation(dataPath));
80
97
  }
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Read-only facade for Bunqueue 2.8.59 Workflow Engine observability.
2
+ * Read-only facade for Bunqueue 2.9.0 Workflow Engine observability.
3
3
  * Persistence, decoding and validation live behind this stable public module.
4
4
  */
5
5
  export { workflowExecution } from './workflow/detail';