bunqueue-dashboard 1.0.1 → 1.0.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.
Files changed (84) hide show
  1. package/README.md +13 -10
  2. package/agent/backup/routes.ts +20 -12
  3. package/agent/manager/config.ts +2 -1
  4. package/agent/server/handler.ts +2 -1
  5. package/agent/server/router.ts +6 -1
  6. package/agent/server/types.ts +1 -1
  7. package/agent/workflow/runtime.ts +25 -7
  8. package/agent/workflow/runtimeRoutes.ts +4 -1
  9. package/dist/.vite/manifest.json +500 -489
  10. package/dist/assets/{AddJob-CiwWycYG.js → AddJob-jq-VZ_YE.js} +1 -1
  11. package/dist/assets/{Alerts-DzKKoA9U.js → Alerts-Bi8zLKXV.js} +1 -1
  12. package/dist/assets/{AreaChart-Db2UVnaK.js → AreaChart-BMYp7z5y.js} +1 -1
  13. package/dist/assets/{Benchmark-CHYDL31K.js → Benchmark-CWYdqm55.js} +1 -1
  14. package/dist/assets/{BulkAddJobs-GRMVnf0j.js → BulkAddJobs-0cdWqkEF.js} +1 -1
  15. package/dist/assets/{Button-_aSJvRoW.js → Button-BCjgbAhY.js} +1 -1
  16. package/dist/assets/{Card-egIDLwVk.js → Card-DptfvL2O.js} +1 -1
  17. package/dist/assets/{Cron-XL0PdUEc.js → Cron-dPGwJXqY.js} +1 -1
  18. package/dist/assets/{CronManager-Cqtg9Zr8.js → CronManager-C0-Y2QDg.js} +1 -1
  19. package/dist/assets/{Database-BoxVpjts.js → Database-CseJOrvD.js} +1 -1
  20. package/dist/assets/{Diagnostics-B3QSQuFF.js → Diagnostics-cZTy4Y2t.js} +1 -1
  21. package/dist/assets/{Dlq-DHOfgORa.js → Dlq-W2xOJiZM.js} +1 -1
  22. package/dist/assets/{DlqControl-BY1e55-x.js → DlqControl-CIV5278D.js} +1 -1
  23. package/dist/assets/DlqPro-BuRxGtGt.js +1 -0
  24. package/dist/assets/{Fleet-Bkbq0dFB.js → Fleet-0zKJSjCw.js} +1 -1
  25. package/dist/assets/{Flows-CDCQrPit.js → Flows-CEfm102Y.js} +1 -1
  26. package/dist/assets/{JobInspector-Bw3Gs1Iy.js → JobInspector-BlMj5-Vr.js} +1 -1
  27. package/dist/assets/{Jobs-IrlneOy6.js → Jobs-D_TId_Oa.js} +1 -1
  28. package/dist/assets/JobsPro-A06ZG5Xi.js +1 -0
  29. package/dist/assets/{Logs-DG7cK-tF.js → Logs-hbh7XhQU.js} +1 -1
  30. package/dist/assets/{LogsPro-ClSqFKys.js → LogsPro-BgEIjgfj.js} +1 -1
  31. package/dist/assets/McpServer-ffXr4nFW.js +22 -0
  32. package/dist/assets/{Metrics-BkePZiCZ.js → Metrics-CIKZGgf0.js} +1 -1
  33. package/dist/assets/{MetricsPro-ooSvpQ3H.js → MetricsPro-CK3WfLCl.js} +1 -1
  34. package/dist/assets/{Overview-CuWmPRra.js → Overview-BBgwyUn4.js} +1 -1
  35. package/dist/assets/{PageHeader-CQzNsw8r.js → PageHeader-DD4vnVog.js} +1 -1
  36. package/dist/assets/{Pagination-CSkzhNW6.js → Pagination-Cj7GzYNX.js} +1 -1
  37. package/dist/assets/{QueueActions-BBxRt-t0.js → QueueActions-BgdW95rN.js} +1 -1
  38. package/dist/assets/{QueueControl-C01l6bBJ.js → QueueControl-g-9kwrpt.js} +1 -1
  39. package/dist/assets/{QueueDetail-gSf9Tb7G.js → QueueDetail-Cv3MLDoE.js} +1 -1
  40. package/dist/assets/{QueueDetailPro-BKszqI0M.js → QueueDetailPro-AhdOAzEF.js} +1 -1
  41. package/dist/assets/{Queues-BAg2nSz0.js → Queues-Bbvh_OCE.js} +1 -1
  42. package/dist/assets/{QueuesOverview-BLSjiYsA.js → QueuesOverview-sPQILfMU.js} +1 -1
  43. package/dist/assets/{S3Backup-Ci4dJPP_.js → S3Backup-Cg6sc2JG.js} +1 -1
  44. package/dist/assets/{S3BackupPro-DcjlrzC5.js → S3BackupPro-BWV_R_Vj.js} +1 -1
  45. package/dist/assets/ServerControl-Dr2gXbaU.js +3 -0
  46. package/dist/assets/{Settings-DR_iqW0T.js → Settings-BsDDlu-h.js} +1 -1
  47. package/dist/assets/{StatusBadge-DereLzOj.js → StatusBadge-cYUkK-4P.js} +1 -1
  48. package/dist/assets/{Usage-ZWfB7PcR.js → Usage-Cxx6pZfb.js} +1 -1
  49. package/dist/assets/{UsagePro-BjdeS2zb.js → UsagePro-CkC8WYcS.js} +1 -1
  50. package/dist/assets/{Webhooks-BonYUgMo.js → Webhooks-BKWnALGC.js} +1 -1
  51. package/dist/assets/{Workers-C1h6hCPp.js → Workers-SJ9MApx3.js} +1 -1
  52. package/dist/assets/WorkersPro-BDgHBTpY.js +1 -0
  53. package/dist/assets/Workflows-Cp6jqKeM.js +1 -0
  54. package/dist/assets/{api-D9aT0ND7.js → api-Bbo86MB6.js} +1 -1
  55. package/dist/assets/{bq-DR2BTISO.js → bq-CSX8DSME.js} +1 -1
  56. package/dist/assets/{classicJobsData-Csj4iihY.js → classicJobsData-BimIyQky.js} +1 -1
  57. package/dist/assets/{connectionStore-CVjQyIZv.js → connectionStore-DdLzJyut.js} +1 -1
  58. package/dist/assets/{controlConnectionEpoch-ChAeMMLk.js → controlConnectionEpoch-D4HtriLM.js} +1 -1
  59. package/dist/assets/{feedback-CtGN_kqB.js → feedback-Dxfj-Fy1.js} +1 -1
  60. package/dist/assets/{form-DG3X6cLL.js → form-Dy-an7ew.js} +1 -1
  61. package/dist/assets/{icons-DH9SS1DA.js → icons-C1HVBnFn.js} +1 -1
  62. package/dist/assets/{index-Bat1DN5U.js → index-CxzFSZj3.js} +3 -3
  63. package/dist/assets/index-DZXf58c2.css +1 -0
  64. package/dist/assets/install-DUkXE5kb.js +3 -0
  65. package/dist/assets/package-C7CtgBEy.js +1 -0
  66. package/dist/assets/react-vendor-DfC0iyIS.js +11 -0
  67. package/dist/assets/{toastStore-XmMX7A5W.js → toastStore-DnqA_aL-.js} +1 -1
  68. package/dist/assets/{useControlActionGuard-1i7aFA7E.js → useControlActionGuard-DImMgJtj.js} +1 -1
  69. package/dist/assets/{usePolledData-CEyyLRYa.js → usePolledData-Dp_ynL9f.js} +1 -1
  70. package/dist/assets/{useServerActionGuard-CUx8TOAC.js → useServerActionGuard-Cr4KUOeZ.js} +1 -1
  71. package/dist/assets/{useThroughputSeries-CeFx-Mwq.js → useThroughputSeries-CrTeR9s3.js} +1 -1
  72. package/dist/index.html +14 -14
  73. package/package.json +40 -33
  74. package/scripts/bunVersion.ts +1 -1
  75. package/scripts/embedded.gen.ts +146 -144
  76. package/dist/assets/DlqPro-CfwKDcpc.js +0 -1
  77. package/dist/assets/JobsPro-vlPd6Ows.js +0 -1
  78. package/dist/assets/McpServer-DoNFY0Pj.js +0 -24
  79. package/dist/assets/ServerControl-SiZBNo9P.js +0 -3
  80. package/dist/assets/WorkersPro-B0f_Nfdz.js +0 -1
  81. package/dist/assets/Workflows-CqZZ96EK.js +0 -1
  82. package/dist/assets/index-C9yGKhQ8.css +0 -1
  83. package/dist/assets/install-oydLpc1T.js +0 -3
  84. package/dist/assets/react-vendor-CFL9l8LK.js +0 -11
package/README.md CHANGED
@@ -32,13 +32,15 @@ The live demo uses sample data. The docs explain how to connect and operate a re
32
32
  ![Vite 8](https://img.shields.io/badge/Vite-8-646CFF?logo=vite&logoColor=white)
33
33
  ![Bun](https://img.shields.io/badge/Bun-1.4-000?logo=bun&logoColor=white)
34
34
  ![TypeScript](https://img.shields.io/badge/TypeScript-strict-3178C6?logo=typescript&logoColor=white)
35
- ![Oxlint + Oxfmt](https://img.shields.io/badge/Oxc-Oxlint%20%2B%20Oxfmt-7C3AED)
35
+ ![Oxlint + Bun fmt](https://img.shields.io/badge/Tooling-Oxlint%20%2B%20Bun%20fmt-7C3AED)
36
36
 
37
37
  <br />
38
38
 
39
- <img src="docs/public/tour.gif" alt="A guided tour of the bunqueue dashboard, clicking through the overview, queues, jobs, the dead-letter queue, flows, the SQLite inspector, and the AI Copilot" width="900" />
39
+ <a href="https://www.youtube.com/watch?v=1bQRFXGClcc" title="Watch the full bunqueue dashboard tour on YouTube"><img src="docs/public/video-tour.png" alt="Watch the narrated 4K tour of bunqueue dashboard on YouTube" width="900" /></a>
40
40
 
41
- <sub>A quick tour. <a href="https://egeominotti.github.io/bunqueue-dashboard/">Try it live →</a></sub>
41
+ ### [▶ Watch the full tour on YouTube](https://www.youtube.com/watch?v=1bQRFXGClcc)
42
+
43
+ <sub>A narrated 4K walkthrough (7:45, English with subtitles) recorded against a real bunqueue server under live traffic. <a href="https://egeominotti.github.io/bunqueue-dashboard/">Or try the live demo →</a></sub>
42
44
 
43
45
  <br /><br />
44
46
 
@@ -134,7 +136,7 @@ contracts; it never patches Bunqueue internals.
134
136
 
135
137
  ## Quick start
136
138
 
137
- **Prerequisites:** [Bun](https://bun.sh) 1.4.2 and a reachable bunqueue server (or let the control
139
+ **Prerequisites:** [Bun](https://bun.sh) 1.4.4 and a reachable bunqueue server (or let the control
138
140
  agent start one for you from the **Server** page).
139
141
 
140
142
  ### Run from npm (no clone)
@@ -261,20 +263,21 @@ target-pinned Flow, Workflow, Queue and Backup agent operations.
261
263
  | `bun start` | **Agent + dashboard together** (one-command dev) |
262
264
  | `bun dev` | Dashboard only (Vite dev server) |
263
265
  | `bun run agent` | Control agent only |
264
- | `bun run build` | Typecheck (`tsc --noEmit`) + production build → `dist/` |
266
+ | `bun run build` | Native Bun typecheck + production build → `dist/` |
267
+ | `bun run typecheck` | Native `bun check` for the frontend, agent/scripts, examples and browser tests |
265
268
  | `bun run preview` | Preview the production build |
266
269
  | `bun run lint` | Lint with Oxlint, including type-aware rules and the implicit-`any` parity guard |
267
- | `bun run format:check` | Check formatting with Oxfmt |
268
- | `bun run check` | Oxlint + Oxfmt validation (the CI gate) |
269
- | `bun run check:fix` | Apply safe Oxlint fixes, then format with Oxfmt |
270
+ | `bun run format:check` | Check formatting with native `bun fmt --check` |
271
+ | `bun run check` | Native typechecking, Oxlint and native formatting validation |
272
+ | `bun run check:fix` | Apply safe Oxlint fixes, then format with native `bun fmt` |
270
273
  | `bun test` | Unit + agent-lifecycle tests |
271
274
  | `bun run test:e2e:tls` | Queue, Flow and Workflow over verified native TLS, including untrusted-certificate rejection |
272
- | `bun run test:e2e:upgrade` | Real npm Bunqueue 2.9.2 SQLite schema-35 → 2.9.4 schema-37 migration and retention semantics |
275
+ | `bun run test:e2e:upgrade` | Real npm Bunqueue 2.9.2 SQLite schema-35 → 2.9.20 schema-37 migration and retention semantics |
273
276
  | `bun run test:e2e` | Real TLS, SQLite upgrade, Flow, Workflow, Queue SDK, and three-broker PostgreSQL runtime tests |
274
277
  | `bun run test:e2e:browser` | Production UI tests on Chromium, Firefox and WebKit |
275
278
  | `bun run test:e2e:browser:postgres-fleet` | Real UI operations across three PostgreSQL brokers (requires Docker and Chromium) |
276
279
  | `bun run quality` | Complete static, build, docs, coverage, runtime, package and dependency gate |
277
- | `bun run test:e2e:postgres-fleet` | Three authenticated Bunqueue 2.9.4 brokers + three agents sharing disposable PostgreSQL 18.6 |
280
+ | `bun run test:e2e:postgres-fleet` | Three authenticated Bunqueue 2.9.20 brokers + three agents sharing disposable PostgreSQL 18.6 |
278
281
 
279
282
  ## Docker
280
283
 
@@ -190,18 +190,26 @@ function assertDatabaseSnapshot(value: unknown, current: DbStats): void {
190
190
  throw new Error('A database snapshot is required to authorize restore');
191
191
  }
192
192
  const expected = value as Record<string, unknown>;
193
- const allowed = ['path', 'exists', 'size', 'walSize', 'shmSize', 'totalSize', 'mtimeMs'];
194
- const unknown = Object.keys(expected).find((key) => !allowed.includes(key));
193
+ const allowed = ['path', 'exists', 'size', 'walSize', 'shmSize', 'totalSize', 'mtimeMs'] as const;
194
+ const unknown = Object.keys(expected).find((key) => !(allowed as readonly string[]).includes(key));
195
195
  if (unknown) throw new Error(`Unknown database snapshot option: ${unknown}`);
196
- if (
197
- expected.path !== current.path ||
198
- expected.exists !== current.exists ||
199
- expected.size !== current.size ||
200
- expected.walSize !== current.walSize ||
201
- expected.shmSize !== current.shmSize ||
202
- expected.totalSize !== current.totalSize ||
203
- expected.mtimeMs !== current.mtimeMs
204
- ) {
205
- throw new Error('Database changed after restore confirmation; reload status and confirm again');
196
+ // Compare what reflects the data. The -shm file is SQLite's shared-memory WAL
197
+ // index: it appears and disappears as connections open and close (the last one
198
+ // deletes it) without any write, so it, and the total that includes it, would
199
+ // reject valid confirmations. Writes still move the main file or the WAL.
200
+ const compared = ['path', 'exists', 'size', 'walSize', 'mtimeMs'] as const;
201
+ // Name every field that moved so a rejected restore is diagnosable.
202
+ const changed = compared
203
+ .filter((field) => expected[field] !== current[field])
204
+ .map((field) => `${field} ${shown(expected[field])} → ${shown(current[field])}`);
205
+ if (changed.length > 0) {
206
+ throw new Error(
207
+ `Database changed after restore confirmation (${changed.join(', ')}); reload status and confirm again`
208
+ );
206
209
  }
207
210
  }
211
+
212
+ function shown(value: unknown): string {
213
+ const text = String(value);
214
+ return text.length > 120 ? `${text.slice(0, 117)}...` : text;
215
+ }
@@ -1,3 +1,4 @@
1
+ import manifest from '../../package.json';
1
2
  import type { ServerConfig } from './types';
2
3
 
3
4
  const CONFIG_KEYS = new Set<keyof ServerConfig>([
@@ -10,7 +11,7 @@ const CONFIG_KEYS = new Set<keyof ServerConfig>([
10
11
 
11
12
  export function defaultConfig(): ServerConfig {
12
13
  return {
13
- command: process.env.BUNQUEUE_START_CMD || 'bunx bunqueue@2.9.4 start',
14
+ command: process.env.BUNQUEUE_START_CMD || `bunx bunqueue@${manifest.dependencies.bunqueue} start`,
14
15
  httpPort: Number(process.env.HTTP_PORT) || 6790,
15
16
  tcpPort: Number(process.env.TCP_PORT) || 6789,
16
17
  dataPath: process.env.BUNQUEUE_DATA_PATH || './data/bunq.db',
@@ -65,7 +65,8 @@ export function createFetchHandler(
65
65
  return new Response(null, { status: 204, headers: corsHeaders(origin, allowedOrigins) });
66
66
  }
67
67
  const mutating = method === 'POST' || method === 'PUT';
68
- const authRequired = requireTokenForAll || (mutating && Boolean(token));
68
+ const configurationRead = pathname === '/control/config' || pathname === '/control/status';
69
+ const authRequired = requireTokenForAll || ((mutating || configurationRead) && Boolean(token));
69
70
  if (authRequired && !tokenOk(request, token)) {
70
71
  return json({ ok: false, error: 'Unauthorized' }, 401, origin);
71
72
  }
@@ -123,12 +123,16 @@ export async function routeAgentRequest(
123
123
  return sqliteOnly('S3 backup', currentStorageMode);
124
124
  }
125
125
  return manager.withStoppedMaintenance('restoring a backup', async () => {
126
+ // Measure what the operator confirmed before our own cleanup: releasing
127
+ // the Workflow Engine's connection checkpoints the WAL, which changes the
128
+ // files without any other writer and would void a valid confirmation.
129
+ const confirmedDatabase = await manager.dbStats(restoreConfig.dataPath);
126
130
  await closeWorkflowRuntime(runtime);
127
131
  return executePreparedBackupRequest(
128
132
  prepared,
129
133
  restoreConfig,
130
134
  current.status === 'running',
131
- await manager.dbStats(restoreConfig.dataPath),
135
+ confirmedDatabase,
132
136
  backupRunner,
133
137
  undefined,
134
138
  managedTargetPolicy
@@ -197,6 +201,7 @@ function isWorkflowRuntimeRoute(pathname: string, method: string): boolean {
197
201
  if (method !== 'POST') return false;
198
202
  return (
199
203
  [
204
+ '/workflows/runtime/initialize',
200
205
  '/workflows/runtime/reload',
201
206
  '/workflows/start',
202
207
  '/workflows/recover',
@@ -5,7 +5,7 @@ export interface AgentOptions {
5
5
  allowedOrigins: string[];
6
6
  /** DNS-rebinding defense; compared by hostname only. */
7
7
  allowedHosts?: string[];
8
- /** On loopback, when set, changing requests must present this token. */
8
+ /** On loopback, when set, mutations and complete configuration reads require this token. */
9
9
  token?: string;
10
10
  /** Network exposure: require token on every non-OPTIONS request. */
11
11
  requireTokenForAll?: boolean;
@@ -29,10 +29,12 @@ export interface WorkflowRuntimeStatus {
29
29
  queueName?: string;
30
30
  concurrency?: number;
31
31
  error?: string;
32
+ needsInitialization?: boolean;
32
33
  }
33
34
 
34
35
  export interface WorkflowRuntimePort {
35
- status(config: ServerConfig, activate?: boolean): Promise<WorkflowRuntimeStatus>;
36
+ status(config: ServerConfig, serverRunning?: boolean): Promise<WorkflowRuntimeStatus>;
37
+ initialize(config: ServerConfig): Promise<WorkflowRuntimeStatus>;
36
38
  reload(config: ServerConfig): Promise<WorkflowRuntimeStatus>;
37
39
  start(config: ServerConfig, workflowName: string, input?: unknown): Promise<unknown>;
38
40
  signal(config: ServerConfig, id: string, event: string, payload?: unknown): Promise<void>;
@@ -72,20 +74,32 @@ export class WorkflowRuntime implements WorkflowRuntimePort {
72
74
  private readonly createEngine: WorkflowEngineFactory = (options) => new Engine(options)
73
75
  ) {}
74
76
 
75
- status(config: ServerConfig, activate = true): Promise<WorkflowRuntimeStatus> {
77
+ status(config: ServerConfig, serverRunning = true): Promise<WorkflowRuntimeStatus> {
76
78
  return this.serial(async () => {
77
79
  const modulePath = configuredWorkflowModule(config);
78
80
  try {
79
81
  if (!modulePath) {
80
- await this.closeActive();
81
82
  return { configured: false, ready: false, workflowNames: [] };
82
83
  }
83
- if (!activate) {
84
- await this.closeActive();
84
+ if (!serverRunning) {
85
85
  return stoppedRuntimeStatus(config, modulePath);
86
86
  }
87
- const active = await this.ensure(config);
88
- return readyRuntimeStatus(active);
87
+ const active = this.active;
88
+ if (active?.closeFailure !== undefined) throw quarantinedRuntimeError(active);
89
+ const path = await realpath(modulePath);
90
+ const modified = (await stat(path)).mtimeMs;
91
+ const options = workflowRuntimeOptions(config);
92
+ if (active?.signature === workflowRuntimeSignature(config, path, modified, options)) {
93
+ return readyRuntimeStatus(active);
94
+ }
95
+ return {
96
+ configured: true,
97
+ ready: false,
98
+ needsInitialization: true,
99
+ moduleName: basename(modulePath),
100
+ workflowNames: [],
101
+ ...options,
102
+ };
89
103
  } catch (error) {
90
104
  const active = this.active;
91
105
  return {
@@ -101,6 +115,10 @@ export class WorkflowRuntime implements WorkflowRuntimePort {
101
115
  });
102
116
  }
103
117
 
118
+ initialize(config: ServerConfig): Promise<WorkflowRuntimeStatus> {
119
+ return this.serial(async () => readyRuntimeStatus(await this.ensure(config)));
120
+ }
121
+
104
122
  reload(config: ServerConfig): Promise<WorkflowRuntimeStatus> {
105
123
  return this.serial(async () => {
106
124
  await this.closeActive();
@@ -49,6 +49,9 @@ export async function routeWorkflowRuntimeRequest(
49
49
  );
50
50
  }
51
51
  if (!serverRunning) throw new Error('Start the managed Bunqueue server before workflow control.');
52
+ if (pathname === '/workflows/runtime/initialize') {
53
+ return success(await admitted(true, ({ config: current }) => runtime.initialize(current)));
54
+ }
52
55
  if (pathname === '/workflows/runtime/reload') {
53
56
  return success(await admitted(true, ({ config: current }) => runtime.reload(current)));
54
57
  }
@@ -98,7 +101,7 @@ function isRuntimePath(pathname: string, method: string): boolean {
98
101
  if (method === 'GET') return pathname === '/workflows/runtime';
99
102
  if (method !== 'POST') return false;
100
103
  return (
101
- ['/workflows/runtime/reload', '/workflows/start', '/workflows/recover', '/workflows/archive', '/workflows/cleanup'].includes(pathname) ||
104
+ ['/workflows/runtime/initialize', '/workflows/runtime/reload', '/workflows/start', '/workflows/recover', '/workflows/archive', '/workflows/cleanup'].includes(pathname) ||
102
105
  /^\/workflows\/[^/]+\/(signal|resume-compensation|abandon-compensation)$/.test(pathname)
103
106
  );
104
107
  }