--- url: /bunqueue-dashboard/docs/quickstart.md description: >- Get the bunqueue dashboard running in one command, then point it at a bunqueue server or let the control agent start one for you. --- # Quickstart Get the dashboard running in under a minute, then connect it to a bunqueue server. Prefer to look before you install? Open the [live demo](https://egeominotti.github.io/bunqueue-dashboard/), the full dashboard running on sample data with no server needed. ## Prerequisites * [Bun](https://bun.sh) 1.4.2, pinned by the repository, CI and published package. * A bunqueue server to drive, or let the control agent start one for you (step 3). ## The one-liner: run from npm No clone, no build, the [`bunqueue-dashboard` npm package](https://www.npmjs.com/package/bunqueue-dashboard) ships the prebuilt dashboard and installs the exact compatible Bunqueue client: ```bash bunx bunqueue-dashboard ``` Open **`http://127.0.0.1:8080`**. One process serves the UI, proxies `/api/*` to your bunqueue server (`BUNQUEUE_URL`, default `http://localhost:6790`), and runs the [control agent](/agent) on `127.0.0.1:6800` so the **Server** page can start / stop / restart bunqueue for you. Configure with env vars: `PORT` · `BIND_ADDR` · `BUNQUEUE_URL` · `AGENT_PORT` · `AGENT_ALLOWED_ORIGINS` · `AGENT_ALLOWED_HOSTS` · `AGENT_TOKEN` · `BUNQUEUE_TOKEN` · `TRUST_PROXY` · `BASE_PATH` · `BUNQUEUE_MANAGED` · `BUNQUEUE_START_CMD`. LAN and reverse-proxy deployments require an explicit Host/origin allowlist plus `AGENT_TOKEN` for `/agent` and `BUNQUEUE_TOKEN` for `/api`; see [PM2 deployment](/deploy/pm2). To install it permanently instead of running via `bunx`: ```bash bun add -g bunqueue-dashboard bunqueue-dashboard ``` Then jump to [step 3](#_3-connect-a-server). Prefer to hack on it or run the dev setup? Take the source route: ## From source ### 1. Install ```bash git clone https://github.com/egeominotti/bunqueue-dashboard.git cd bunqueue-dashboard bun install ``` ### 2. Run ```bash bun start ``` One command boots the control agent and the dashboard together, and stops both on `Ctrl-C`: | Service | URL | Role | | --- | --- | --- | | Dashboard | `http://localhost:5273` | the UI (`/api` is proxied to `:6790`) | | Control agent | `http://127.0.0.1:6800` | starts / stops / restarts the server process | Open **`http://localhost:5273`**. ## 3. Connect a server Two ways, pick either: * **Let the agent start one.** Open **Control ▸ Server**, set the launch command, and press **Start**. The dashboard manages the process and tails its logs live. See [Server Control](/guide/server). * **Point at existing servers.** Open **[Settings](/guide/settings)** and add one profile per Bunqueue API + paired agent (with their separate tokens), or seed the first server URL at build time with `VITE_BUNQUEUE_URL`. For multiple PostgreSQL-backed brokers, verify and operate them from [Fleet](/guide/fleet). When the all-in-one dashboard connects to a broker owned by systemd, Docker or Kubernetes, launch it with `BUNQUEUE_MANAGED=0 BUNQUEUE_URL=http://127.0.0.1:6790`. The Server page becomes an attach-only health view and all child-process controls are removed. Backup restore also fails closed because only the external supervisor can prove that the broker has released its database. ## Next steps * Take the [illustrated tour of every screen](/user-guide). * [Add your first job](/guide/add-job), then watch it in the [Jobs Explorer](/guide/jobs). * Ship it: [Docker, Kubernetes, PM2, or a hosting platform](/deploy/). * Press **Cmd / Ctrl-K** anywhere for the command palette. ::: tip Already have a server? The dashboard never modifies Bunqueue. Most screens use its HTTP API; Flow, Workflow and backup operations go through the loopback agent using the pinned public Bunqueue client/CLI, so point the agent at the same server and data path. ::: --- --- url: /bunqueue-dashboard/docs/user-guide.md description: >- An illustrated, section-by-section tour of the bunqueue dashboard, one detailed, screenshot-backed page per dashboard section. --- # User guide The guides below explain each dashboard section: displayed data, available actions, state-dependent controls, API calls and known limitations. Screenshots illustrate the main operator pages; use the [verification matrix](/testing) for current test coverage. Screenshots were captured against a live seeded server (queues `emails`, `image-resize`, `reports`, `notifications`, `benchmark`, `maintenance`; real completed jobs, DLQ entries, cron schedules, webhooks and workers) in the default dark theme. ::: tip Two page families, by design The **Pro** pages (`src/pages/control/*`, client `lib/bq.ts`) are the complete control surface and own the sidebar, they're documented below. The first-generation **classic** pages remain reachable at `*-classic` routes and are covered in the [Classic pages appendix](/guide/classic). For the full route → component → API-client table, see [Pages & routes](/pages). ::: ## Home * [**Overview**](/guide/overview), the landing page: connection banner, headline stat cards, per-queue health grid, and a live activity feed. * [**Fleet**](/guide/fleet), health and lifecycle control for multiple Bunqueue brokers, paired agents, and shared PostgreSQL topology. ## Queues * [**Queues**](/guide/queues), the fleet view: every queue with per-state counts and inline pause/resume. * [**Jobs Explorer**](/guide/jobs), server-paginated jobs for a queue, with filters, multi-select and Promote as the only job-lifecycle mutation. * [**Dead Letter Queue**](/guide/dlq), cross-queue DLQ dashboard: reasons, attempt history and CSV export; every retry and purge path fails closed. * [**Cron Jobs**](/guide/cron), list and create scheduled jobs (cron expression or interval). ## Workflow * [**Workflow Engine**](/guide/workflows), a dedicated command centre with Overview, Executions, Waiting & Signals, Compensation, and Archive views. * [**Job Flows**](/guide/flows), DAG exploration, all five FlowProducer create modes, dependency/result tools, and safe Flow Job methods. ## Monitoring * [**Metrics**](/guide/metrics), rolling throughput chart, queue-depth trend, latency percentiles, per-queue counts. * [**Workers**](/guide/workers), registered workers, active/stale status, and guarded stale-worker registry cleanup (it does not stop worker processes). * [**Logs**](/guide/logs), the live SSE event feed with filters and search. * [**Alerts**](/guide/alerts), browser-local thresholds, triggered rules and delivery limits. ## Control * [**Server Control**](/guide/server), start / stop / restart the bunqueue process via the local control agent, with live logs. * [**Add Job**](/guide/add-job), enqueue a job with payload, options, priority and delay. * [**Bulk Add Jobs**](/guide/bulk-add), import validated JSON or NDJSON into one queue. * [**Job Inspector**](/guide/job-inspector), a single job's full timeline, payload, result and state-gated actions. * [**Queue Control**](/guide/queue-control), per-queue actions, live Queue SDK limits/deduplication/metrics/journal tools, and stall/DLQ configuration; DLQ retention is read-only. * [**DLQ Control**](/guide/dlq-control), read-only single-queue dead-letter triage. * [**Webhooks**](/guide/webhooks), register, enable/disable and remove webhook endpoints. * [**Diagnostics**](/guide/diagnostics), connectivity and health checks. * [**Benchmark**](/guide/benchmark), drive synthetic load and watch throughput. ## Management * [**Database**](/guide/database), the read-only SQLite inspector: tables, schema, rows, and query runner. * [**MCP Server**](/guide/mcp), install and connect the separate stdio server. * [**Copilot**](/guide/copilot), configure a model and approve safe queue actions. * [**Usage**](/guide/usage), cumulative usage totals and runtime/storage facts. * [**S3 Backup**](/guide/s3), live configuration, status, listing, on-demand snapshots, and guarded restore through the local control agent. * [**Settings**](/guide/settings), named broker/agent profiles, isolated session credentials, polling interval, and theme. ## Appendix * [**Classic pages**](/guide/classic), the first-generation view pages, kept intact, and the 404 catch-all. --- --- url: /bunqueue-dashboard/docs/development.md description: >- Set up, run, build, and test the bunqueue dashboard locally: the one-command dev launcher, the quality gate, and the project layout. --- # Development This repository uses Bun **1.4.2** everywhere. `.bun-version` is the canonical pin consumed by local version managers and every GitHub Actions workflow; `bun run version:check` rejects drift in the runtime, package metadata, Docker base image, type definitions, CI, or publish job. New here? Start with the [Quickstart](quickstart.md) for a guided first run, then use the [deployment overview](deploy/index.md#how-the-dashboard-finds-your-server) to choose how the dashboard connects to bunqueue. Control-agent environment variables are documented under [Control agent configuration](agent.md#configuration). This page is the day-to-day workflow: run, gate, and how to add a page additively. ## Run ```bash bun install bun start # agent + dashboard together (Ctrl-C stops both) ``` `bun start` (`scripts/dev.ts`) is the one-command path. Prefer separate processes? The granular commands still work: ```bash bun run agent # control agent (start/stop/restart) → 127.0.0.1:6800 bun dev # dashboard → http://localhost:5273 ``` Point it at a server via **Settings** (or `VITE_BUNQUEUE_URL`). In dev, `/api/*` is proxied to `http://localhost:6790`. ## Quality gate ```bash bun run quality ``` This is the exact blocking gate run by the [CI workflow](https://github.com/egeominotti/bunqueue-dashboard/actions/workflows/ci.yml) on every push and pull request. Release, Pages, and Docker run the same command before publishing. It executes, in order: * `bun run version:check`: enforce the Bun version pin. * `bun run architecture`: enforce the TypeScript source file size budget. * `bun run check`: Oxlint plus an Oxfmt formatting check (`bun run check:fix` applies safe lint fixes, then formats files). * `bun run build`: strict typechecks for `src/`, `agent/`, `scripts/`, examples and browser fixtures, then the production Vite build. * `bun run size`: initial-load and total JavaScript bundle budgets. * `bun run docs:build`: the VitePress production build, including dead-link checks. * `bun run test:coverage`: the complete Bun test suite plus aggregate coverage floors. * `bun run test:e2e`: disposable Bunqueue 2.9.4 TLS, SQLite migration, Flow/Workflow/Queue contracts plus three authenticated brokers and agents sharing PostgreSQL 18.6 (Docker required). * `bun run test:package`: packs, installs, starts, and probes the published binary from a clean temporary consumer. * `bun run audit:high`: a blocking dependency audit for HIGH and CRITICAL advisories. CI also runs `bun run test:e2e:browser` as a separate blocking matrix on Chromium, Firefox, and WebKit. The suite uses the production bundle, a non-root `BASE_PATH`, an authenticated disposable Bunqueue 2.9.4 process, and a temporary database. It verifies the token gate, full sidebar navigation, SSE reconnection after an actual upstream restart, confirmed Cron mutations, and automated WCAG A/AA rules. Operational browser tests also submit and inspect jobs, import bulk jobs, reconcile a 40-job benchmark, execute SQLite queries, operate webhook entries, inspect DLQ failures, receive live logs and evaluate alert rules. For a local first run: ```bash bun run test:e2e:browser:install bun run test:e2e:browser ``` For multi-node UI verification, also run `bun run test:e2e:browser:postgres-fleet` (Docker and Chromium required). This command is separate from `quality` and the regular browser matrix. The Chromium CI job also runs `bun run test:e2e:browser:managed` for managed lifecycle, Workflow, Flow, MCP, local S3 and scripted Copilot transport/tool checks. See [Testing & verification](testing.md) for the coverage of each section and external service prerequisites. `bun run test:e2e:docs` separately checks clean documentation URLs on desktop/mobile and search/navigation after the VitePress build. The audit has one ID-specific exception: `GHSA-qwww-vcr4-c8h2` affects React Router's RSC mode. This project is a client-only `BrowserRouter` SPA and has no RSC request handler or server actions, so that advisory is not applicable. The exception does not suppress any other advisory; a new HIGH or CRITICAL finding fails the gate. Remove it if the app adopts RSC, or when a compatible patched React Router release becomes available. Notes: * `.oxlintrc.json` and `.oxfmtrc.json` are the committed root configurations for Oxlint and Oxfmt. Oxlint runs the JavaScript, TypeScript, Oxc, Unicorn, React, and JSX accessibility plugins with the project's curated severities; `oxlint-tsgolint` provides the type-aware rules. The lint script also retains Biome's implicit-`any` declaration check, which Oxlint does not yet implement. Oxfmt keeps the established two-space, 100-column, single-quote style and deterministic imports. `src/index.css` (Tailwind v4 at-rules), `agent/`, and `scripts/` are excluded from formatting and linting. They are not skipped by typechecking: `tsconfig.json` covers `src/`, while `tsconfig.agent.json` covers both Bun runtime directories (except the generated `scripts/embedded.gen.ts`). * `bunfig.toml` preloads `test/setup.ts` (a `localStorage` shim) so store imports work under `bun test`. ## Adding a page (additive) 1. Create `src/pages/control/MyPage.tsx` (a new file). Use `bq` for data, the `ui/*` kit for layout (see [components.md](components.md)), `usePolledData` for polling. 2. Wire it in `src/App.tsx` (a new ``). 3. Add a nav item in `src/components/layout/Sidebar.tsx`'s `NAV` array (reuse an existing icon or add one to `ui/icons`). 4. If the page shows a job/queue state, drive its state-dependent buttons off `lib/jobActions.ts::actionGates` rather than re-deriving which actions are dashboard-authorized, see [api-mapping.md](api-mapping.md#job-action-gating). Upstream endpoint acceptance alone is not authorization: the shared gates intentionally keep DLQ retry and completed-job requeue false. **Both steps 2 and 3 are required**: a route with no nav entry (or vice versa) is a dead end. The route-completeness tests compare the registered routes, sidebar destinations, and page-title map; do not leave a new page outside that contract. **Do not rewrite existing pages or the `api.ts` client.** Corrected behaviour goes in a new page using `bq`. If you find a live bug while working nearby, check [known-issues.md](known-issues.md) first, it may already be tracked, and add it there if not, rather than silently patching something out of scope. ## Conventions * Data: `usePolledData(() => bq.x())` returns `{ data, error, loading, refetching, refetch }`. Render `LoadingState` on first load, `ErrorState` on failure with data absent, otherwise the content (keep last data while refreshing). * Actions: call `bq.*` then `refetch()`; guard destructive ops with `window.confirm`; surface failures inline. * Formatting: use `lib/format` (`formatNumber` uses `.` thousands; times are relative; durations from `startedAt`/`completedAt`). * Styling: Tailwind tokens (`bg-surface`, `text-muted`, `border-line`, `text-accent`), `.tnum` for numbers, mono for IDs. * Keep files focused; prefer new small components over growing a page past ~300 lines. ## Tests Use `bun test` for a fast local iteration and `bun run test:coverage` for the same suite with the CI coverage floor. Tests live under `test/` and cover pure logic, stores and clients, component regressions, SSE parsing, and control-agent behaviour. Add focused regression coverage there for every bug fix or new testable behaviour. ## Dependency patch `ai` is pinned to **7.0.14** because `patches/ai@7.0.14.patch` handles a rejected browser telemetry completion promise on cancellation. Keep `patches/` alongside `package.json` and `bun.lock` before running a frozen install, including in Docker. When upgrading the SDK, verify the managed Copilot Stop regression against the production browser bundle before removing or refreshing the patch. --- --- url: /bunqueue-dashboard/docs/testing.md description: >- Reproduce real Bunqueue dashboard tests and understand the evidence available for each section. --- # Testing & verification A page rendering successfully is a navigation check. An operation is verified when a real server accepts it and the resulting data or state is observed. The dashboard uses both types of checks, plus component and contract tests for races, malformed responses and disabled actions. The public demo uses fixtures; it does not prove connectivity or persistence. ## Reproduce locally Use the pinned Bun **1.4.2**, installed dependencies, Docker, and Playwright browser binaries: ```bash bun install --frozen-lockfile bun run test:e2e:browser:install bun run quality bun run test:e2e:browser bun run test:e2e:browser:postgres-fleet bun run test:e2e:browser:managed bun run test:e2e:docs ``` `quality` includes version and architecture checks, lint/format, typechecks, production and docs builds, bundle budgets, coverage, real runtime E2E, package installation and restart smoke tests, a short database resilience run, and the HIGH/CRITICAL dependency audit. Playwright runs as separate jobs in `.github/workflows/validation.yml`: Chromium, Firefox and WebKit, with PostgreSQL Fleet, managed-server and documentation scenarios in Chromium. Every publishing workflow calls that complete validation workflow and waits for all jobs. CI exposes one `Stability gate` status for branch protection; failed, cancelled or skipped validation cannot pass it. The active main-only ruleset requires a pull request, resolved review threads and this check from GitHub Actions against an up-to-date branch. Its declarative configuration is `.github/main-ruleset.json`. The validation workflow also compiles and executes each release binary on its native runner: Ubuntu x64/arm64, macOS x64/arm64 and Windows x64. The smoke test checks embedded assets, BASE\_PATH, authentication, API proxying, SQLite browsing/query/export, timeout recovery, configuration persistence and shutdown with no Bun executable on PATH. GitHub Release downloads these exact tested artifacts for the same SHA. Windows process termination uses TerminateProcess; POSIX additionally verifies graceful signal handling. Native x64 CI provides the x64 evidence. Coverage floors are 88% of logic lines and 89% of logic functions. Critical lifecycle, policy, configuration and database modules also have individual floors in `scripts/criticalCoveragePolicy.ts`. For repeatable resilience checks: ```bash bun run build:bin bun run test:binary BQ_SOAK_SECONDS=1800 BQ_SOAK_ROWS=1000000 bun run test:soak BQ_RECONNECT_CYCLES=100 bun run test:e2e:browser --project=chromium --grep=resilience ``` The short gate uses 100,000 rows and at least 30 seconds. The scheduled/manual resilience workflow uses one million rows for 30 minutes plus 100 actual upstream restarts in Chromium. It records agent RSS (in a separate process from the load generator), control latency, pool/child cleanup and browser heap usage, and fails on unexpected responses or unbounded growth. These are regression budgets, not publishable benchmarks. The 128 MiB growth budget uses Bun's native memory footprint measurement, which excludes macOS pages already returned to the kernel; RSS is also logged as a diagnostic. Platforms without that measurement fall back to RSS. Measurements do not force garbage collection. All databases, configuration snapshots and test containers are disposable. The browser fixture starts an authenticated Bunqueue **2.9.4** server and the production all-in-one dashboard under `/e2e/dashboard`. It uses a temporary SQLite database, loopback ports **49380–49384**, and test-only tokens. Keep those ports free and run only one regular or managed browser suite at a time. The fixture shuts down its children and removes its database on exit. No existing local broker or application database is used. The PostgreSQL scenarios need a working Docker daemon and `postgres:18.6-alpine` (downloaded on first use). They create a disposable container, namespace, three brokers and three agents. The regular browser fixture attaches to an external test process; Fleet tests exercise managed process controls. An external-mode disabled Start/Stop button is expected behavior. The managed browser suite uses the same reserved ports plus **49390** for a disposable local MinIO S3 endpoint. It starts the pinned Bunqueue CLI through the agent, registers the test Workflow module, and verifies UI commands against that server. MinIO has no host mounts and uses only synthetic local credentials. Its container is removed afterward; a pre-existing image is retained. No cloud bucket is used. The local model fixture is a scripted HTTP provider, not a language model. For focused browser work after a production build: ```bash bun run build bun scripts/gen-embed.ts bunx playwright test e2e/operations.e2e.ts --project=chromium bunx playwright test e2e/monitoring.e2e.ts --project=chromium ``` ## Section coverage All **30 sidebar destinations**, the classic pages, the legacy Cron redirect and the 404 fallback are visited on Chromium, Firefox and WebKit. The table describes the additional behavioral evidence; it does not claim every possible button and configuration is covered. | Section | Real verification | Scope or prerequisite | | --- | --- | --- | | Overview | Authenticated dashboard data; SSE recovery after an actual broker restart | `dashboard.e2e.ts` | | Fleet | Three healthy APIs, shared PostgreSQL target/namespace, cross-node state | PostgreSQL browser scenario | | Queues and queue detail | Submitted queue appears; detail opens; pause/resume observed across brokers | Operations + Fleet browser tests | | Jobs and Job Inspector | UI-created payload readback; priority/delay changes, promotion, log add/clear and unavailable destructive actions | Operations, managed and Fleet browser tests | | Add Job and Bulk Add | Single submission, two-job JSON import, exact waiting count; group admission rejection | Operations + Fleet browser tests | | Dead Letter Queue and DLQ Control | Deliberate unrecoverable job failure appears in both views; purge remains disabled | Monitoring browser tests; no production failure data | | Cron Jobs | Cancelled confirmation does not mutate; confirmed create/delete; schedule visible across nodes | Dashboard + Fleet browser tests | | Workflow Overview, Executions, Waiting & Signals, Compensation, Archive | UI start, durable signal, resume/abandon compensation, archive/history, reload, orphan recovery and terminal cleanup | Managed browser suite with real registered handler module; `test:e2e:workflow` covers runtime contracts | | Job Flows | All five FlowProducer create modes, fourteen inspection methods and a safe log mutation through the UI | Managed browser suite plus all safe runtime operations in `test:e2e:flow` | | Metrics | Seed queue appears in per-queue metrics and telemetry connects | Monitoring browser tests | | Workers | MCP registration/heartbeat/unregister; active record displayed; naturally stale idle record removed through UI | Managed browser suite; registry-only fixtures do not run consumer processes | | Logs | Newly enqueued job arrives via SSE; search hides and restores the row | Monitoring browser tests | | Alerts | Browser rule evaluates a real waiting job, triggers, and is deleted | Browser must stay open; no email/Slack delivery | | Server | Managed start/stop/restart, generation and process logs | Managed and Fleet browser suites plus lifecycle tests | | Queue Control | Stall/DLQ policy save, delayed promotion, Queue SDK metrics and journal trim; shared limits and group policy | Managed, Fleet and `test:e2e:queue-operations` suites | | Webhooks | Create, disable and delete a real registry entry; independent API readback | Unused queue and reserved `.invalid` URL; outbound delivery not exercised | | Diagnostics | Server version and Ping; real GC, heap statistics and independent readiness probes | Operations and managed browser tests | | Benchmark | 40 jobs produced and processed by two simulated workers; server confirms 40 completed, zero waiting/active/failed | Operations browser test on each browser engine | | Database | Real SQLite schema, row filter, CSV export, SQL result and rejected write with unchanged data | Managed browser suite; PostgreSQL is not a SQLite target | | MCP | Actual stdio client discovers 73 tools, 5 resources and 3 prompts; stats resource and health prompt read | Pinned public executable with explicit optional SDK; not all 73 tools executed | | Usage | Runtime counters and healthy storage response render | Monitoring browser tests | | S3 Backup | UI config, real upload/list to local MinIO, stopped-server restore and readback proving a later job disappeared | Managed browser suite; cloud-specific authentication and networking not exercised | | Settings | Theme/poll interval survive reload; three profiles saved and each connection tested | Managed and Fleet browser suites; credentials remain session-only | | Copilot (launcher) | Streaming, declined/confirmed mutation, Stop without mutation or unhandled errors, Clear chat | Scripted local provider with real queue writes; no model inference or paid provider requests | | Classic pages | All eleven classic routes render, including queue detail | Navigation coverage, not a duplicate mutation suite | ## Current regression additions (0.0.45) The managed suite adds **13 Chromium scenarios** against Bunqueue **2.9.4**. It supplements the existing **42 tests** across Chromium, Firefox and WebKit and the three-broker PostgreSQL scenario. The commands above are the reproducible source of results; CI retains diagnostics when a scenario fails. Two browser issues found by these tests are fixed: the Copilot custom Base URL field now has an explicit accessible label, and the AI SDK's browser telemetry completion promise is handled when Stop aborts a turn. The latter is a minimal, version-pinned Bun patch in `patches/ai@7.0.14.patch`, applied during a frozen install and tested in the production browser bundle. It does not suppress global browser errors or alter tool confirmation. Bunqueue's MCP TCP registration can return ID `"0"` despite storing another ID. That upstream limitation and the verified registry lookup workaround are recorded in [MCP setup](/guide/mcp#verified-tcp-worker-limitation). ## Documentation browser checks `bun run test:e2e:docs` builds VitePress and tests every generated content page at its clean URL on desktop (1440 px) and mobile (390 px). It checks the hydrated page, browser errors and document overflow, then exercises search, navigation and Back. The fixture reserves loopback port **49556** and runs in Chromium CI. Static dead-link checks run during `docs:build`; browser checks additionally catch client routing failures that a valid HTML response alone cannot detect. The custom view-transition wrapper preserves VitePress's router options and skips the initial load. Dropping `initialLoad` previously caused direct clean URLs to hydrate into the 404 page, even though the server returned the right HTML. This regression is covered by the URL checks above. ## Results recorded on 2026-09-05 The verification uses Dashboard **0.0.43**, Bun **1.4.0**, Bunqueue **2.9.4**, and PostgreSQL **18.6** (schema **20**) for Fleet. These are dated results, not a guarantee for later commits. * The canonical quality gate passed: **1,050 Bun tests**, **0 failures**, with aggregate logic coverage **89.82% lines / 90.75% functions** (the configured floor excludes TSX). * Real TLS, SQLite schema 35 → 37 migration, Flow, Workflow, Queue SDK, PostgreSQL Fleet and packed-package runtime checks passed. * **42 browser tests passed in 43.6 seconds** across Chromium, Firefox and WebKit: navigation, accessibility and eight additional operational scenarios per browser. The benchmark reconciled **120 completed jobs** across the three engines (40 each). * The PostgreSQL UI scenario passed after removing an obsolete hard-coded 2.9.3 version expectation. It now compares the connection result with the version probed on the real broker. ## Evidence and troubleshooting Playwright prints each scenario and browser result. Failures retain screenshots, videos and traces in `test-results/`; open a trace with `bunx playwright show-trace `. Runtime scenarios print their assertions and JSON summaries. To retain local evidence: ```bash bun run quality > /tmp/dashboard-quality.log 2>&1 bun run test:e2e:browser > /tmp/dashboard-browser.log 2>&1 bun run test:e2e:browser:postgres-fleet > /tmp/dashboard-fleet-browser.log 2>&1 ``` If authentication appears after a full reload, enter the test/server token again: credentials are held in memory and are not persisted to browser storage. A reserved webhook URL only tests registry operations. Verify outbound delivery/signatures, cloud-specific S3 access and actual Copilot model answers in a dedicated environment with those services configured, and record the provider, model and results separately. Local S3 restore and MCP checks are in the managed suite. See [known limitations](known-issues.md) before interpreting intentionally disabled actions as failures. --- --- url: /bunqueue-dashboard/docs/deploy.md description: >- Deploy the bunqueue dashboard as a static site or a single all-in-one server, with Docker, Kubernetes, PM2, or a hosting platform. Two modes, one decision. --- # Deployment The dashboard is a **static single-page app**. That makes it cheap and easy to host: build it once, serve the files anywhere. The only real decision is **how the browser reaches your bunqueue server**, and that gives you two modes. ## Two modes ### 1. Static site (recommended for most) Build the SPA and serve the `dist/` folder from any static host or the Docker image (Caddy). There is **no backend** in this mode, the browser talks to your bunqueue server directly. Good for: Vercel, Netlify, Cloudflare Pages, GitHub Pages, S3, the Docker image behind your own proxy, Kubernetes. ### 2. All-in-one server (SPA + API proxy + control agent) One process (`scripts/serve.ts`) that serves the SPA, proxies `/api/*` to your bunqueue server **same-origin** (no CORS), and runs the **control agent** so you can start / stop / restart the bunqueue process from the UI. Three ways to get it: ```bash bunx bunqueue-dashboard # from npm, installs dependencies, needs Bun 1.4.2 ``` or download a **standalone binary** from the [GitHub Releases](https://github.com/egeominotti/bunqueue-dashboard/releases) (no runtime needed at all), or run it from a source checkout. Good for: a VM or box where you run bunqueue itself and want full process control. Run it under **[PM2](/deploy/pm2)**, systemd, or Docker. ::: tip Which one? If you just want to **view and drive an existing** bunqueue server, use the **static site**. If you want the dashboard to also **manage the server process** (start / stop / restart with live logs), use the **all-in-one server**, the control agent only exists there. ::: ## How the dashboard finds your server Every request goes to a **base URL**, resolved in this order: 1. `VITE_BUNQUEUE_URL` baked in at **build time**, else 2. `/api` (a same-origin path that something must proxy), and 3. whatever you type on the in-app **[Settings](/guide/settings)** page wins at runtime (it is saved in the browser). So you have two wiring strategies: | Strategy | How | Trade-off | | --- | --- | --- | | **Direct (cross-origin)** | Set `VITE_BUNQUEUE_URL` to the server's public origin, e.g. `https://queue.example.com` | Simplest to deploy. The bunqueue server must allow the dashboard's origin (CORS). | | **Same-origin proxy** | Serve the dashboard and forward `/api/*` to bunqueue from the same host | No CORS. The **all-in-one server** does this automatically; with the Caddy image, add a `reverse_proxy` (see [Docker](/deploy/docker#same-origin-api-proxy)). | If the bunqueue server needs a bearer token (`AUTH_TOKENS`), enter it in Settings for the current browser session or authenticate at the front proxy. Never bake a secret into `VITE_*`; build-time values are public bundle text. For a LAN/reverse-proxied **all-in-one server**, also configure `BUNQUEUE_TOKEN`: `/api/*` otherwise returns `403`, and every request must carry that bearer. Enter the same value as the Server token in Settings. If Bunqueue uses `AUTH_TOKENS`, configure that value upstream too because Authorization is forwarded. This gate belongs to the all-in-one server; static hosting still depends on Bunqueue or the front proxy for authentication. ### Existing externally supervised server The all-in-one process normally owns the Bunqueue child lifecycle. If systemd, Docker or Kubernetes already owns that process, make the relationship explicit: ```bash BUNQUEUE_MANAGED=0 \ BUNQUEUE_URL=http://127.0.0.1:6790 \ BUNQUEUE_TOKEN="$SERVER_TOKEN" \ bunx bunqueue-dashboard ``` In this attach-only mode, **Control ▸ Server** probes `BUNQUEUE_URL/health`, labels the server **External**, and exposes no Start, Stop, Restart or launch configuration controls. The agent rejects those lifecycle/config mutations with HTTP 409 as a second line of defense. Stopped-only backup restore receives the same fail-closed response because external process state cannot be proven. ### All-in-one server below a URL prefix `BASE_PATH` is the runtime equivalent of a static build's `VITE_BASE`. It moves the SPA router, embedded assets, same-origin API proxy and agent bridge together: ```bash BASE_PATH=/internal/queue bunx bunqueue-dashboard ``` Preserve the prefix when forwarding to the all-in-one listener: ```nginx location /internal/queue/ { proxy_pass http://127.0.0.1:8080; } ``` The browser then uses `/internal/queue/assets/*`, `/internal/queue/api/*` and `/internal/queue/agent/*`; deep-link refreshes stay inside the mount. Requests outside the configured mount return 404. `BASE_PATH` accepts a leading-slash, URL-safe path and normalizes its trailing slash. Target-pinned Flow, Workflow, Queue and Backup requests accept only that exact prefixed API alias and still resolve it through the local anti-SSRF check. ## Pick your target | You want to | Go to | | --- | --- | | Run a container | [Docker](/deploy/docker) | | Deploy to a cluster | [Kubernetes](/deploy/kubernetes) | | Keep a long-running process alive on a box | [PM2](/deploy/pm2) | | Push to Vercel / Netlify / Cloudflare / Fly / Render / Cloud Run | [Hosting platforms](/deploy/platforms) | ## Build output, in one place ```bash bun install bun run build # → dist/ (static SPA: index.html + assets/) ``` * Deploying under a **sub-path** (e.g. `example.com/dashboard/`)? Build with `VITE_BASE=/dashboard/ bun run build`. * Baking in the server origin? `VITE_BUNQUEUE_URL=https://queue.example.com bun run build`. * The static SPA needs a **history-API fallback**: every unknown path must return `index.html`, or deep links and refreshes 404. Each platform page below shows how. --- --- url: /bunqueue-dashboard/docs/deploy/docker.md description: >- Run the bunqueue dashboard as a Docker container, served by Caddy. Pull the published image, run with docker or docker compose, or build your own. --- # Docker The image is a **multi-stage build**: Bun compiles the SPA, then **Caddy** serves the static files (gzip + zstd, SPA history fallback, immutable asset caching). It listens on port **80**. ## Run the published image Every push to `main` publishes `edge`; tagged releases publish `latest` and the semver tag. ```bash docker run --rm -p 8080:80 ghcr.io/egeominotti/bunqueue-dashboard:edge # → http://localhost:8080 ``` Then open **[Settings](/guide/settings)** and point it at your bunqueue server, or bake the origin in at build time (below). ::: tip Pin a version in production Prefer `:latest` or a specific `:vX.Y.Z` over `:edge` for anything you depend on. ::: ## docker compose ```yaml services: dashboard: image: ghcr.io/egeominotti/bunqueue-dashboard:latest ports: - "8080:80" restart: unless-stopped ``` ```bash docker compose up -d ``` ## Build your own image Bake a default server origin in at build time so users do not have to set it in Settings: ```bash docker build \ --build-arg VITE_BUNQUEUE_URL=https://queue.example.com \ -t bunqueue-dashboard . docker run --rm -p 8080:80 bunqueue-dashboard ``` `VITE_BUNQUEUE_URL` is a **build argument**, not a runtime env var: the value is compiled into the bundle. To change it later, rebuild (or override it at runtime from the Settings page). ## Same-origin API proxy By default the image is a **pure static server**: the browser calls your bunqueue server directly, so that server needs CORS for the dashboard's origin. To avoid CORS entirely, serve the dashboard and proxy `/api/*` to bunqueue from the **same** origin. Extend the Caddyfile: ```txt :80 { root * /usr/share/caddy encode gzip zstd header { Content-Security-Policy "frame-ancestors 'none'; object-src 'none'; base-uri 'self'" X-Frame-Options "DENY" X-Content-Type-Options "nosniff" Referrer-Policy "no-referrer" } # Forward /api/* to the bunqueue server, stripping the /api prefix. handle_path /api/* { reverse_proxy bunqueue:6790 } @assets path /assets/* header @assets Cache-Control "public, max-age=31536000, immutable" try_files {path} /index.html file_server } ``` Leave `VITE_BUNQUEUE_URL` **unset** so the client uses the default `/api` path, which Caddy now proxies. `handle_path` strips `/api`, so `/api/dashboard` reaches bunqueue as `/dashboard`, exactly like the dev proxy and the all-in-one server. ::: warning Authenticate a public API proxy This route exposes bunqueue's administrative API. Enable bunqueue `AUTH_TOKENS` and enter its token at runtime, or require authentication at the front proxy. Host/Origin routing and CORS are not user authentication, and a token must never be baked into a public `VITE_*` build value. ::: Mount the file over the image's default and put both containers on one network: ```yaml services: dashboard: image: ghcr.io/egeominotti/bunqueue-dashboard:latest ports: ["8080:80"] volumes: - ./Caddyfile:/etc/caddy/Caddyfile:ro bunqueue: image: your/bunqueue-server # exposes :6790 on the internal network ``` ## Health check The image ships a `HEALTHCHECK` that curls `/` every 30s, so orchestrators see `healthy` / `unhealthy` out of the box. ## Want process control too? This image is static and has **no control agent**, so Server Control (start / stop / restart) is not available in it, by design (a hosted static site should not spawn processes). For the full control surface, run the **[all-in-one server](/deploy/pm2)** where the dashboard also manages the bunqueue process. --- --- url: /bunqueue-dashboard/docs/deploy/kubernetes.md description: >- Deploy the bunqueue dashboard to Kubernetes with a Deployment, Service, and Ingress. Copy-paste manifests, health probes, and TLS notes. --- # Kubernetes The dashboard is a stateless static container, so a plain **Deployment + Service + Ingress** is all you need. It scales horizontally with zero shared state. ## Manifests Save as `dashboard.yaml` and `kubectl apply -f dashboard.yaml`. ```yaml apiVersion: apps/v1 kind: Deployment metadata: name: bunqueue-dashboard labels: { app: bunqueue-dashboard } spec: replicas: 2 selector: matchLabels: { app: bunqueue-dashboard } template: metadata: labels: { app: bunqueue-dashboard } spec: containers: - name: dashboard image: ghcr.io/egeominotti/bunqueue-dashboard:latest ports: - containerPort: 80 readinessProbe: httpGet: { path: /, port: 80 } initialDelaySeconds: 5 periodSeconds: 10 livenessProbe: httpGet: { path: /, port: 80 } initialDelaySeconds: 10 periodSeconds: 20 resources: requests: { cpu: 10m, memory: 32Mi } limits: { cpu: 250m, memory: 128Mi } --- apiVersion: v1 kind: Service metadata: name: bunqueue-dashboard spec: selector: { app: bunqueue-dashboard } ports: - port: 80 targetPort: 80 --- apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: bunqueue-dashboard annotations: cert-manager.io/cluster-issuer: letsencrypt-prod spec: ingressClassName: nginx tls: - hosts: [dashboard.example.com] secretName: bunqueue-dashboard-tls rules: - host: dashboard.example.com http: paths: - path: / pathType: Prefix backend: service: name: bunqueue-dashboard port: { number: 80 } ``` The container's Caddy config already returns `index.html` for unknown paths, so React Router deep links work behind the Ingress with no extra rewrite rules. ## Pointing at your bunqueue server Two options, same as everywhere: * **Direct:** build a custom image with `--build-arg VITE_BUNQUEUE_URL=https://queue.example.com` and use it in place of the published one. The bunqueue server needs CORS for the dashboard host. * **Same-origin:** run bunqueue in the cluster and route `/api` to it. Either add a `reverse_proxy` to the container's Caddyfile via a **ConfigMap** mounted at `/etc/caddy/Caddyfile` (see [Docker](/deploy/docker#same-origin-api-proxy)), or add a second Ingress `path: /api` pointing at the bunqueue Service. No CORS needed. ## Health and scaling * The `readinessProbe` keeps traffic off a pod until Caddy is serving; the `livenessProbe` restarts a wedged one. * The image is tiny and stateless, so bump `replicas` freely or attach a `HorizontalPodAutoscaler`. There is no session affinity to worry about. ::: warning No control agent in the cluster The container is the **static** build, so Server Control is not available. Do not try to run the all-in-one server (with the control agent) in a shared cluster: it is loopback-bound and meant to manage a bunqueue process on the **same host**, not across pods. ::: --- --- url: /bunqueue-dashboard/docs/deploy/pm2.md description: >- Run the all-in-one bunqueue dashboard server under PM2, with SPA, same-origin API proxy, and the control agent. Ecosystem file, env vars, and startup on boot. --- # PM2 Use this when you run bunqueue on a box and want the dashboard to **also manage the process** (start / stop / restart with live logs). PM2 keeps the **all-in-one server** alive, restarts it on crash, and brings it back on reboot. The all-in-one server (`scripts/serve.ts`) does three jobs in one process: 1. serves the dashboard SPA, 2. proxies `/api/*` to your bunqueue server **same-origin** (no CORS), and 3. runs the **control agent** on `127.0.0.1` for Server Control. ## Get the server Pick one: ```bash # A) Install from npm (needs Bun 1.4.2; dependencies are installed normally). bun add -g bunqueue-dashboard # then: bunqueue-dashboard # one-off, no install: bunx bunqueue-dashboard # B) Download a standalone binary from the GitHub Releases (no runtime needed). # Assets: bunqueue-dashboard--- (linux/macos x64+arm64, windows x64) # Replace with the latest release tag, e.g. v0.0.15: curl -L -o bunqueue-dashboard \ https://github.com/egeominotti/bunqueue-dashboard/releases/latest/download/bunqueue-dashboard--linux-x64 chmod +x bunqueue-dashboard # C) Build the binary yourself (needs Bun 1.4.2). bun run build:bin # → ./bunqueue-dashboard # D) Run from source (needs Bun 1.4.2). bun run scripts/serve.ts ``` ## Configure it All configuration is via environment variables: | Variable | Default | Purpose | | --- | --- | --- | | `PORT` | `8080` | Port the dashboard + `/api` proxy listen on | | `BIND_ADDR` | `127.0.0.1` | Interface the dashboard binds to; set `0.0.0.0` for direct LAN access (no reverse proxy) | | `BUNQUEUE_URL` | `http://localhost:6790` | The bunqueue server to proxy to | | `AGENT_PORT` | `6800` | Control agent port (always bound to `127.0.0.1`) | | `AGENT_TOKEN` | *unset* | Agent bearer token; required on every `/agent/*` request when LAN/proxy access is configured | | `BUNQUEUE_TOKEN` | *unset* | Admin-API bearer; required on every `/api/*` request when LAN/proxy access is configured | | `AGENT_ALLOWED_ORIGINS` | dev + local origins | Exact external browser origins; their hostnames are also admitted by the Host gate | | `AGENT_ALLOWED_HOSTS` | loopback names | Extra Host header names/IPs admitted on every dashboard route | | `TRUST_PROXY` | *unset* | Set `1` only when a trusted proxy overwrites `X-Forwarded-Host` because it rewrites `Host` | | `LOG_LEVEL` | `info` | pino log level (`debug` / `info` / `warn` / `error`) | ::: tip Secure the agent The direct agent port only binds `127.0.0.1`, but the all-in-one server also bridges it at `/agent`. Any non-loopback bind, trusted-proxy mode, forwarding header, or explicit non-loopback Host/origin switches that bridge to remote policy: `AGENT_TOKEN` is then mandatory on **reads and writes**. Enter it when the dashboard lock screen prompts; it stays in browser memory for that session. The same remote policy disables `/api/*` unless `BUNQUEUE_TOKEN` is configured; enter that value as the Server token in Settings. The two tokens are separate credentials and should be rotated independently. ::: ## PM2 ecosystem file Save as `ecosystem.config.cjs` next to the binary: ```js module.exports = { apps: [ { name: 'bunqueue-dashboard', script: './bunqueue-dashboard', // the compiled binary env: { PORT: 8080, BUNQUEUE_URL: 'http://localhost:6790', AGENT_TOKEN: 'change-me', BUNQUEUE_TOKEN: 'change-api-token', // Required for https://dashboard.example.com through a proxy that // preserves Host. Replace this with the exact external origin. AGENT_ALLOWED_ORIGINS: 'https://dashboard.example.com', }, autorestart: true, max_restarts: 10, }, ], }; ``` Running from source or from the npm global install instead of the binary? Use Bun as the interpreter: ```js // from source: script: 'scripts/serve.ts', interpreter: 'bun', // from npm global: script: 'bunqueue-dashboard', interpreter: 'bun', ``` ## Reverse proxy Prefer preserving the external Host. This nginx shape needs no `TRUST_PROXY`; `AGENT_ALLOWED_ORIGINS` in the PM2 example both admits the Host and declares the exact browser origin: ```nginx server { listen 443 ssl; server_name dashboard.example.com; location / { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $http_host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` If the proxy must rewrite `Host`, its replacement Host must also be loopback or listed in `AGENT_ALLOWED_HOSTS`. It must **overwrite** `X-Forwarded-Host` with the external authority; then opt in to trusting that value: ```nginx proxy_set_header Host 127.0.0.1:8080; proxy_set_header X-Forwarded-Host $http_host; ``` ```js TRUST_PROXY: '1', AGENT_ALLOWED_ORIGINS: 'https://dashboard.example.com', AGENT_TOKEN: 'change-me', BUNQUEUE_TOKEN: 'change-api-token', ``` Never pass through a client-supplied `X-Forwarded-Host` when `TRUST_PROXY=1`. The Host allowlist is still evaluated against the rewritten raw `Host` before the forwarded value is considered. ::: warning Protect the API proxy too `/api/*` forwards Bunqueue's administrative HTTP API. A LAN/proxied all-in-one server fails closed until `BUNQUEUE_TOKEN` is set, and then requires that bearer on every request. Enter the same value in Settings. If Bunqueue itself enables `AUTH_TOKENS`, make it one of those tokens because Authorization is forwarded upstream. Host and Origin checks remain CSRF/rebinding defenses, not auth. ::: ## Direct LAN access A wildcard bind cannot infer which LAN IP or alias clients will put in `Host`. List every reachable origin (or list bare names/IPs in `AGENT_ALLOWED_HOSTS`), and configure the mandatory agent token: ```js BIND_ADDR: '0.0.0.0', AGENT_ALLOWED_ORIGINS: 'http://192.168.1.50:8080,http://dashboard.lan:8080', AGENT_TOKEN: 'change-me', BUNQUEUE_TOKEN: 'change-api-token', ``` Without that allowlist, `/`, assets, `/api` and `/agent` all fail closed with `403 Host not allowed`. A fixed `BIND_ADDR=192.168.1.50` admits that concrete address automatically, but aliases still need to be listed. ## Start, persist, boot ```bash pm2 start ecosystem.config.cjs pm2 save # remember the process list pm2 startup # print the command to start PM2 on boot, then run it ``` Useful day-to-day: ```bash pm2 logs bunqueue-dashboard # tail logs pm2 restart bunqueue-dashboard # after a config change pm2 status # health at a glance ``` Open `http://localhost:8080` (or your reverse-proxied domain). Because `/api` is proxied same-origin, there is no CORS to configure, and **Server Control** works because the control agent runs in the same process. On LAN/proxy access, the first agent request prompts for `AGENT_TOKEN`; the credential is kept only in memory and must be re-entered after a reload. Enter `BUNQUEUE_TOKEN` in the Server-token prompt/Settings for `/api`; it is also held only in browser memory. ## Prefer systemd? The binary is a plain executable, so a unit works just as well: ```ini [Unit] Description=bunqueue dashboard After=network.target [Service] ExecStart=/opt/bunqueue-dashboard/bunqueue-dashboard Environment=PORT=8080 Environment=BUNQUEUE_URL=http://localhost:6790 Environment=AGENT_TOKEN=change-me Environment=BUNQUEUE_TOKEN=change-api-token Restart=on-failure [Install] WantedBy=multi-user.target ``` --- --- url: /bunqueue-dashboard/docs/deploy/platforms.md description: >- Deploy the bunqueue dashboard to Vercel, Netlify, Cloudflare Pages, GitHub Pages, Render, Fly.io, Railway, or Google Cloud Run. Copy-paste config for each. --- # Hosting platforms The dashboard builds to a static `dist/` folder, so it drops onto any host. Two families: **static hosts** (serve the files) and **container hosts** (run the Docker image). Everywhere, the build is the same: ```bash bun install && bun run build # → dist/ ``` Set `VITE_BUNQUEUE_URL` to your server's origin (a build-time env var on the platform), or leave it and configure the target from the in-app **[Settings](/guide/settings)** page. On a static host the browser calls bunqueue **directly**, so that server needs **CORS** for your dashboard domain. Every static host also needs a **history-API fallback** (serve `index.html` for unknown paths) or deep links 404. That is the one config each platform below sets. ## Static hosts ### Vercel `vercel.json` in the repo root: ```json { "buildCommand": "bun run build", "outputDirectory": "dist", "rewrites": [{ "source": "/(.*)", "destination": "/index.html" }] } ``` Set `VITE_BUNQUEUE_URL` under Project Settings, Environment Variables. Deploy with `vercel --prod` or connect the Git repo. ### Netlify `netlify.toml` in the repo root: ```toml [build] command = "bun run build" publish = "dist" [[redirects]] from = "/*" to = "/index.html" status = 200 ``` ### Cloudflare Pages * Build command: `bun run build` * Build output directory: `dist` * SPA fallback: add a `public/_redirects` file (Vite copies it into `dist`): ``` /* /index.html 200 ``` ### GitHub Pages Served under a repo sub-path, so build with the base path and add a `404.html` that mirrors `index.html` (Pages serves it for unknown routes): ```bash VITE_BASE=/your-repo/ bun run build cp dist/index.html dist/404.html ``` Publish `dist/` with `actions/deploy-pages`. This repo already does exactly this for the docs site in `.github/workflows/pages.yml`. ### Render (static site) * Build command: `bun install && bun run build` * Publish directory: `dist` * Add a rewrite rule: Source `/*`, Destination `/index.html`, Action **Rewrite**. ## Container hosts These run the [Docker image](/deploy/docker) (Caddy already handles the SPA fallback, so there is nothing extra to configure). ### Fly.io ```bash fly launch --image ghcr.io/egeominotti/bunqueue-dashboard:latest ``` Ensure `fly.toml` has `internal_port = 80`, then `fly deploy`. Fly terminates TLS for you. ### Railway New project, Deploy from the repo (Railway builds the `Dockerfile`), or Deploy an image and paste `ghcr.io/egeominotti/bunqueue-dashboard:latest`. Set the service port to **80**. ### Google Cloud Run ```bash gcloud run deploy bunqueue-dashboard \ --image ghcr.io/egeominotti/bunqueue-dashboard:latest \ --port 80 \ --allow-unauthenticated \ --region europe-west1 ``` Cloud Run injects TLS and a public URL, and scales to zero when idle. ## Want the API proxy and process control? All of the above serve the **static** build (no control agent, browser talks to bunqueue directly). For a same-origin `/api` proxy **and** Server Control, run the **[all-in-one server](/deploy/pm2)** on a VM (PM2 / systemd) or as a container, and point these platforms' proxy or your load balancer at it. --- --- url: /bunqueue-dashboard/docs/guide/overview.md description: >- A single screen that shows, at a glance, whether your bunqueue server is healthy and what your queues are doing right now. --- # Overview A single screen that shows, at a glance, whether your bunqueue server is healthy and what your queues are doing right now. **Where:** Home (the landing page). ![Overview](../screenshots/overview.png) ## What you'll see At the top, a wide banner tells you if the dashboard is connected. Below it are two rows of headline numbers, a grid of your busiest queues, and a live feed of recent job events. Everything updates on its own, you don't need to refresh. **Connection banner** | State | What it means | | --- | --- | | Green dot · "bunqueue server connected" · **Online** | The latest check reached the server; the numbers are fresh. | | Amber dot · "Connection lost, showing last known data" · **Stale** | The latest check failed; you're seeing the last numbers received, which may be a little old. | The line under the banner always shows the server address, how long it's been running (uptime), and how much memory it's using. **Health row** | Element | What it tells you | | --- | --- | | Error Rate | Recorded failed jobs divided by recorded processed jobs; unknown with no sample, red above 5%. | | Failed | Current failed jobs summed across queues. | | DLQ | Current dead-letter entries. | **Throughput and inventory row** | Element | What it tells you | | --- | --- | | Completed | Retained completed jobs. | | Active | Jobs being processed now. | | Ready backlog | Waiting plus prioritized jobs across queues. | | Push/sec / Pull/sec | Current rates, with process-session totals labelled since restart. | | Queues | Queue count, with active cron count beneath it. | Uptime and memory appear in the connection banner. There is no API Keys card. **Queue Health — most loaded** shows up to six queues ranked by failed jobs, then ready backlog (waiting plus prioritized), rather than arbitrary list order. Each card links to its queue detail. Missing values use a dash (—). **Recent Activity**, a live feed of the last few job events, each with a colored status dot, the queue, a short job ID, the status, and how long ago it happened. ## What you can do This screen is for watching, not changing, there are no destructive actions here. You can: * **Open a queue**, click any Queue Health card to jump into that queue's details. * **See all queues**, click **View All** next to Queue Health. * **See the full activity log**, click **View All** next to Recent Activity. * **Reconnect**, when the banner is amber, click **Retry** to check the server again right away. ## Good to know ::: tip The screen refreshes on its own every few seconds. An amber "Stale" banner means only the *last* check failed, the server may still be up, and your numbers are simply a few seconds old. Click **Retry** to check again. ::: * **Queue Health shows six queues.** If you have more, use **View All** to see them. The **Queues** number still counts every one. * **Recent Activity starts empty.** It fills as new events arrive and doesn't load past history. For the full picture, open the activity log via **View All**. * **A dash (—) means "not available yet,"** not zero. For a plain-language list of current limits, see [Known issues](/known-issues). ::: details Under the hood (for developers) * Uses the shape-verified `bq` client plus a shared activity-stream hook. * Polls two endpoints together, `GET /dashboard` and `GET /queues/summary`, on the global refresh interval (default 3000 ms), with at most one request in flight. * Live events come from the `/events` SSE stream (250-event ring buffer, ~150 ms flush, 2000 ms reconnect backoff). * Deliberately two requests per poll, not one-per-queue: `/queues/summary` already carries every queue's counts. ::: --- --- url: /bunqueue-dashboard/docs/guide/fleet.md description: >- Operate multiple Bunqueue brokers, their paired control agents, and a shared PostgreSQL namespace from one dashboard. --- # Fleet Fleet is the multi-broker control surface. It checks every configured Bunqueue API and its paired control agent without changing the Dashboard's active node, groups brokers that report the same PostgreSQL target and namespace, and lets you start, stop, restart, or select each broker. **Where:** open `/fleet` from the sidebar. ## Three Bunqueue brokers and one PostgreSQL database Use one connection profile per broker and one control agent beside each broker: ```mermaid flowchart LR UI["Dashboard"] A1["Agent A"] --> B1["Bunqueue A"] A2["Agent B"] --> B2["Bunqueue B"] A3["Agent C"] --> B3["Bunqueue C"] UI --> A1 UI --> A2 UI --> A3 UI --> B1 UI --> B2 UI --> B3 B1 --> PG[("PostgreSQL")] B2 --> PG B3 --> PG ``` All three brokers must use the same `BUNQUEUE_POSTGRES_URL` and `BUNQUEUE_POSTGRES_NAMESPACE`. Give each broker unique HTTP/TCP ports and each agent a unique URL. In **Settings**, create three profiles containing the matching Bunqueue API URL, agent URL, server token, and agent token. ## What you can do * See API and agent reachability independently for every node. * Confirm the Bunqueue version, managed process state, storage driver, PostgreSQL target, and namespace. * Detect accidental split topology: different database targets or namespaces appear as separate PostgreSQL groups. * Start a stopped managed broker, or stop/restart a running one, through that broker's paired agent. Stop and Restart require confirmation. * Click **Use node**, or use the node selector under the sidebar, to retarget every other Dashboard page atomically. Old polls are aborted and stale data from the previous broker is hidden before the new node renders. * Refresh all nodes immediately; otherwise Fleet polls every 10 seconds with endpoint-level failure isolation and a 7.5-second deadline. Each profile keeps its server and agent bearer tokens separate. Profile names and URLs survive reloads; tokens are memory-only and must be entered again in a new browser session. ## Shared versus node-local state | Scope | Dashboard behaviour with PostgreSQL | | --- | --- | | Queues, jobs, FlowProducer DAGs, DLQ, crons, worker registry, queue/group limits, deduplication, durable events and metrics | Stored under the PostgreSQL namespace and observable through every healthy broker in that group. Use any member, including after another member created the state. | | Broker lifecycle, launch configuration, health, memory/connections and process logs | Node-local. Fleet sends the command to the selected card's paired control agent. | | Webhooks | Bunqueue 2.9.3 keeps these in the broker process. Configure and inspect them on the intended active node; they are not a PostgreSQL-wide registry. | | Workflow Engine | The Dashboard agent uses that profile's local `dataPath` for Workflow state and module handlers. It is not merged merely because queue storage is PostgreSQL. | | Database inspector and S3 backup | Deliberately unavailable in PostgreSQL mode because both operate on Bunqueue's SQLite file. The pages return an explicit SQLite-only message instead of touching the Workflow database or another local file. | | Alerts and Copilot setup | Browser-local. Their live reads and allowed commands follow the currently active profile. | ## Real compatibility gate The repository's canonical E2E gate starts a disposable PostgreSQL 18.6 container, three Bunqueue 2.9.4 brokers, and three authenticated agents. It proves cross-broker enqueue, inspect, leased pull/ack, pause/resume, cron, and rate-limit operations, then removes every process, container, and temporary file: ```bash bun run test:e2e:postgres-fleet ``` The Benchmark consumer also requests explicit owner/lease tokens and forwards them to heartbeat, acknowledge, and compensation calls, so a PostgreSQL claim remains valid when completion is handled through another broker. ::: warning Upgrade one namespace as a unit Bunqueue 2.9.3 upgrades the published 2.9.2 package's PostgreSQL schema 19 to schema 20. Stop or coordinate every broker that shares a namespace before the upgrade; every member must run 2.9.3 before normal traffic resumes, because a 2.9.2 broker cannot join schema 20\. The disposable three-broker runtime and Dashboard browser scenarios assert schema 20 before testing cross-broker jobs, policies, groups and lifecycle. ::: --- --- url: /bunqueue-dashboard/docs/guide/queues.md description: >- See every queue on your server at a glance, check how each one is doing, and pause or resume any of them in one click. --- # Queues See every queue on your server at a glance, check how each one is doing, and pause or resume any of them in one click. **Where:** open `/queues` from the sidebar. ![Queues](../screenshots/queues.png) ## What you'll see At the top, four summary cards give you the totals across your whole server. Below them, a searchable table lists one row per queue with its live job counts and a status pill. A **Live** badge in the header tells you the numbers keep refreshing on their own. The four summary cards: | Card | What it tells you | | --- | --- | | **Waiting** | Total jobs enqueued and waiting for a worker, across all queues | | **Active** | Total jobs being processed right now | | **Failed** | Total jobs that ran out of retries (turns red when above zero) | | **Paused** | How many queues are currently paused (a queue count, not a job count) | Each row in the table: | Column | What it tells you | | --- | --- | | **Queue** | The queue name, click it to open that queue | | **Waiting** | Jobs waiting for a worker | | **Active** | Jobs being processed now | | **Completed** | Jobs that finished successfully | | **Failed** | Jobs that exhausted their retries (red when above zero) | | **Delayed** | Jobs scheduled to run later | | **Status** | A green **Active** or orange **Paused** pill | | **Actions** | The pause/resume button, plus an arrow into the queue | ::: tip The four summary cards always add up **every** queue, even while you're searching, the totals stay put as you type, so they always reflect the full fleet. ::: ## What you can do * **Pause a queue.** Click the amber pause icon on a row. That queue stops handing work to workers; its button is briefly disabled, the table refreshes, and a confirmation line appears above it. * **Resume a queue.** Click the green play icon on a paused row. Work starts flowing again, with the same quick confirmation. * **Search.** Type in the filter box to narrow the table to queues whose name contains what you typed. It's case-insensitive. * **Open a queue.** Click a row (or the queue name) to jump into that queue's detail view. * **Page through queues.** Use the pager below the table, it shows 15 queues per page. * **Recover from an outage.** If the server drops, a banner appears with a **Retry** button that reconnects. After each pause or resume you'll see a one-line result above the table: a green confirmation (for example, `payments paused ✓`) on success, or a red message explaining what went wrong. Only the row you clicked is disabled while it's working, every other row stays usable. ::: warning Pause and resume act **immediately**, there's no confirmation prompt. A single mis-click can pause a live queue, so check the row's **Status** pill afterward. Flow-destructive Drain/Clean/Obliterate controls are visible but disabled on the detail/control pages because v2.9.3 cannot inspect reverse dependencies. ::: ## Good to know * **The summary cards ignore your search.** They're fleet-wide totals and won't change as you filter, that's intentional, but easy to misread. * **The Paused card counts queues, not jobs.** It tells you how many queues are paused, not how many jobs are held. * **Numbers are as of the last refresh.** With the default few-seconds cadence, and refreshing paused while the browser tab is in the background, a busy queue can lag reality by a moment. The **Live** badge means the data is polled, not streamed instantly. * **Empty states are explicit.** With no queues yet you'll see *"No queues yet."*; a search that matches nothing shows *"No queues match your search."* * **Your place is kept.** If a filter shrinks the list past your current page, you're moved to the last valid page rather than losing your spot. * No known issues are specific to this screen. See [Known issues](/known-issues) for the current, honest list. ::: details Under the hood (for developers) * Loads every queue with its counts in a single `GET /queues/summary` call, no per-queue fan-out. Search, sort, and pagination are all client-side. * Pause/resume post to `POST /queues/:q/pause` and `POST /queues/:q/resume`; a logical `{ ok: false }` surfaces as the red error line. * Polls at the global refresh interval from Settings (default 3000 ms, floored at 500 ms), one request at a time, suspended while the tab is hidden. * Uses the `bq` client throughout. ::: --- --- url: /bunqueue-dashboard/docs/guide/jobs.md description: >- Browse, inspect and export jobs in a queue, with Promote as the only enabled job-lifecycle mutation. --- # Jobs Explorer Browse, inspect and export the jobs in a queue. Delayed jobs can be promoted, while DLQ retry and completed-job requeue fail closed. **Where:** open `/jobs` from the sidebar. ![Jobs Explorer](../screenshots/jobs.png) ## What you'll see A **live** indicator in the header tells you the page is refreshing on its own. At the top, six stat cards summarize the **whole server**. Below them are the filters, then the job table. | Element | What it tells you | | --- | --- | | **Total** | Every job across all states, server-wide. | | **Waiting** | Jobs enqueued but not started yet. | | **Active** | Jobs being processed right now. | | **Completed** | How many jobs have finished successfully (lifetime). | | **Failed** | How many jobs have failed (lifetime). Turns red when it's above zero. | | **Error Rate** | Failures as a share of finished jobs. Green when healthy, red above 5%. | Each row in the table shows one job: | Column | What it tells you | | --- | --- | | **Job ID** | The job's identifier. Hover to see the full ID if it's cut off. | | **Status** | The job's current state as a colored badge. | | **Priority** | **HIGH**, **MEDIUM**, or **LOW**, based on the job's priority value. | | **Created** | When the job was added. | | **Duration** | How long the job took to run. Shows `, ` until the job has both started and finished. | | **Actions** | Inspect, plus any actions the job's state allows. | ::: info The stat cards describe the entire server, so they **won't** match the counts of the queue you have selected below. ::: ## What you can do **Pick a queue.** Use the queue dropdown to choose whose jobs to list. If you arrived from a link with a queue already set, it's pre-selected; otherwise the first queue is chosen for you. **Filter by status.** Switch between `all`, `waiting`, `active`, `completed`, and `failed`. **Search this page by ID.** Type in the ID filter to narrow the rows down to a matching ID. This searches only the rows currently on screen (see Good to know). **Inspect a job.** Click the eye button on any row to open it in the Job Inspector. **Act on a single job.** A delayed row also offers: * **Promote**, move a delayed job to run now. **Act on many jobs at once.** Tick the checkboxes (or the header checkbox to select the whole page) to reveal a bulk toolbar. **Promote selected** appears only when at least one visible selected job is delayed; no Retry or Requeue bulk action is exposed. **Export CSV**, download the rows on the current page. ::: warning Unsafe lifecycle transitions fail closed The dashboard never exposes `DELETE /jobs/:id`. Bunqueue v2.9.3 cannot reveal every reverse flow dependency, so deleting an apparently standalone job can permanently strand another queue's parent. DLQ retry is also unavailable because its GET + POST sequence has no atomic generation/state/topology precondition and can hit a recreated job. Completed-job requeue is unavailable because `retryCompleted` does not rebuild dependency registration or flow order. ::: After any action, the row (or selection) reports success or failure in a short status line above the table, and the list refreshes. Buttons on a busy row are disabled until it finishes. ## Good to know * **The ID filter only searches the current page.** It matches the 25 rows on screen, not the whole queue. To find one specific job in a large queue, use the Job Inspector's direct lookup instead. * **There's no "page X of Y."** You page through 25 jobs at a time. **Next** stays available as long as a full page arrives; a shorter page means you've reached the end. * **Which actions appear depends on the job's state.** A delayed job can be promoted. Active, completed and failed jobs have no state-changing row action; if none of the selected jobs is delayed, the toolbar says so. * **Changing queue, status, or page clears your selection.** This is on purpose, so a bulk action can never hit rows you picked under a different view. * **If the server is unreachable,** a banner with a **Retry** button appears and your already-loaded rows stay visible. * This `/jobs` page is the corrected, server-paginated explorer. A separate legacy jobs page exists but isn't what this screen uses, see [Known issues](/known-issues). ::: details Under the hood (for developers) Everything here uses the shape-verified `bq` client (not the legacy `api` client). * Queue dropdown: `GET /queues/summary`, polled every 30 s. * Stat cards: `GET /dashboard`, polled every 10 s. * Job table: `GET /queues/:q/jobs/list?states=…&limit=25&offset=…`, polled at the global refresh interval (default 3 s, configurable in Settings). The response is flat `{ ok, jobs }` with no `total`, so "next page" is inferred from a full 25-row page. * The only job-lifecycle mutation maps to `POST /jobs/:id/promote`. This page never calls DLQ retry or retry-completed endpoints. ::: --- --- url: /bunqueue-dashboard/docs/guide/dlq.md description: >- Inspect, filter and export failed jobs while every DLQ retry and purge path fails closed under the v2.9.3 contract. --- # Dead Letter Queue This screen is where jobs land after they fail and run out of retries. You can inspect, filter and export failures; every manual, bulk and queue-wide retry control is unavailable under the v2.9.3 fail-closed policy. **Where:** open `/dlq` from the sidebar. ![Dead Letter Queue](../screenshots/dlq.png) ## What you'll see At the top are four summary cards. Below them, a grid of tiles, one per queue that currently has failures, lets you drill into a single queue. Pick a queue and its failed jobs appear in the table underneath, with a toolbar to filter and sort them. Only **Total in DLQ** counts every queue at once. The other three cards describe the queue you've selected, and show a placeholder (`Select a queue` or `, `) until you pick one. | Card | What it tells you | | --- | --- | | **Total in DLQ** | Failed jobs across every queue. The badge reads **Healthy** (green) at zero, **Attention** (red) when there's anything to look at. | | **Top Reason** | The most common failure reason in the selected queue. | | **Pending Retry** | Jobs in the selected queue waiting to be retried automatically, but not yet retried. | | **Failure Types** | How many different failure reasons the selected queue has. | Once you pick a queue, its failed jobs list in a table: | Column | What it tells you | | --- | --- | | **Job ID** | The failed job's ID. Click it to open the job in the inspector. | | **Reason** | Why it failed, shown as a red badge (for example `max_attempts_exceeded`). | | **Error** | The error message, shortened to fit. A dash means no message was recorded. | | **Entered** | How long ago the job landed in the dead letter queue. | | *(last column)* | A disabled retry button; destructive removal is not exposed. | ::: tip Jobs can have first-class names in Bunqueue 2.9, but names are not unique and this table uses the Job ID as the stable failure identifier. Click it to see the name, full timeline, and error detail. ::: ## What you can do * **Pick a queue**, choose it from the dropdown or click a tile. Its failures load, and the reason filter resets to show everything. * **Filter by reason**, narrow the list to a single failure reason. * **Sort by newest or oldest**, order the list by when jobs entered the queue. * **Search by Job ID**, type part of an ID to find a specific failure fast. * **Open a job**, click any Job ID to inspect its full history. * **Page through**, the pager moves 25 entries at a time. * **Export this page**, download the currently loaded entries as CSV. **Retry one job** is visible but disabled. A preliminary exact-ID GET cannot authorize the separate retry POST: the endpoint has no atomic precondition for job generation, state or topology, so the original job can disappear and a different job recreated under the same ID can receive the POST. **Retry all** is visible but disabled for the same reason, with the additional problem that the targeted DLQ can move while the request is in flight. **Purge all** is visible but disabled because deletion can strand hidden cross-queue dependents. ::: warning Atomic safety takes precedence [Bunqueue v2.9.3](https://github.com/egeominotti/bunqueue/releases/tag/v2.9.3) exposes no generation/state/topology-conditional DLQ retry and no reverse-dependency-aware purge. A warning, confirmation, pinned target or fresh queue/job scan cannot make either mutation atomic, so the dashboard calls none of those routes. ::: ## Good to know * **The reason filter, ID search, and sort work on the page you're viewing, not the whole queue.** When a queue has more than one page of failures, these tools only touch the 25 entries currently on screen, the sort labels say "(this page)", and if a filter finds nothing it'll remind you to check other pages with the pager. This is a known limitation of the server, not a bug. See [Known issues](/known-issues). * **The by-queue tiles hide empty queues.** Only queues that actually have failures show up, so the grid stays focused on what needs attention. If nothing has failed anywhere, the grid disappears entirely. * **The three per-queue cards can go blank.** If the queue's stats fail to load, Top Reason, Pending Retry, and Failure Types fall back to placeholders, but the table still works. **Total in DLQ** is always independent. * **Total in DLQ can lag by a few seconds.** It refreshes on a slower cycle than the rest of the page, so after an external mutation or server-side retention event the grand total may take a moment to catch up. * **Every Retry / Retry All / Purge All control remains disabled by design**, even after selecting a queue. * **Individual permanent removal is unavailable.** Bunqueue 2.9's `Queue.removeDlqJob()` still accepts only queue + job ID, which cannot prove a custom ID was not reused for a different generation after the row was read. * **Existing server retention can remove entries without a dashboard action.** `maxEntries` may evacuate entries immediately and `maxAge` drives destructive expiry. Queue Control shows both values read-only and cannot save them. ::: details Under the hood (for developers) * Uses the shape-verified **`bq`** client throughout. * Two polls run in parallel. The queue list (for the grand total, tiles, and dropdown counts) refreshes every **10s** via `GET /dashboard/queues`. The selected queue's entries (`GET /queues/:q/dlq`) and stats (`GET /queues/:q/dlq/stats`) refresh on the global interval (default **3s**, set in Settings); a stats failure is swallowed so the table still renders. * Row, queue-wide and global retry/removal controls are disabled. No DLQ mutation request is sent from this page. ::: --- --- url: /bunqueue-dashboard/docs/guide/cron.md description: >- This screen lets you set up jobs that run on a repeating schedule, then see them all in one place. --- # Cron Jobs This screen lets you set up jobs that run on a repeating schedule, then see them all in one place. **Where:** open `/cron` from the sidebar. ![Cron Jobs](../screenshots/cron.png) ## What you'll see At the top is a **Create schedule via upstream upsert** card. Below it is a table listing every schedule already registered on the server, 15 rows per page. A **live** indicator shows the list is refreshing on its own. Each row is one schedule: | Column | What it tells you | | --- | --- | | **Name** | The schedule's unique name. | | **Queue** | The queue that receives a job every time the schedule fires. | | **Schedule** | When it fires, a cron expression like `0 9 * * *`, or `every ms` for an interval schedule. | | **Next Run** | The local date and time of the next run. | | **Runs** | How many times this schedule has fired so far. | Each row also has a trash icon at the end for deleting that schedule. ## What you can do **Submit a schedule upsert**, fill in the form and click **Submit upsert**: 1. Enter a **Name** (for example `daily-report`) and a **Queue** (for example `reports`). Both are required. 2. Set the **Spawned job name** workers will receive. It defaults to `default` and is separate from schedule data. 3. Pick how it repeats with the mode toggle: * **cron**, enter a **Cron expression** (for example `0 9 * * *` or `0 */10 * * * *` with leading seconds). * **every**, enter an interval in **milliseconds** (a whole number greater than 0). 4. Optionally set **Data (JSON)**, the payload attached to every job this schedule creates. It must be valid JSON; leave it as `{}` if you don't need one. 5. Open **Advanced options** for timezone/priority/execution limits, overlap and restart policy, cron deduplication (`ttl`/`extend`/`replace`), and spawned-job retry, timeout, delay, stall timeout, and removal policy. 6. Click **Submit upsert**. The confirmation explicitly asks you to authorize last-writer-wins behavior. On an exact name/queue acknowledgement the form clears and a green **Cron upsert acknowledged** badge appears. This is an acknowledgement, not proof that no concurrent writer replaced it afterward. The button is disabled while it's working, so a fast double-click can't create the same schedule twice. Creation also re-fetches the cron list immediately before POST and fails closed if the name now exists. ::: warning Upstream creation is not atomic `POST /crons` is an upsert in Bunqueue v2.9.3 and has no create-only or version precondition. The dashboard refuses a name already observed and rechecks immediately before POST, but a simultaneous client can still race between that GET and POST. The UI never claims atomic creation; use a globally unique name and continue only when last-writer-wins is acceptable. ::: **Delete a schedule**, click the trash icon on its row. ::: warning Deleting asks you to confirm first, then removes the schedule permanently. If a delete fails, the reason is shown in a red banner above the form rather than passing silently. ::: ## Good to know * **No editing or pausing.** The form refuses an already observed name; delete it, wait for refresh, then submit a replacement upsert. The unavoidable absent-name race remains documented above. * **Intervals are in milliseconds.** `every 300000ms` is 5 minutes, it's easy to type seconds by mistake. The field only checks that the number is a positive whole number, not that the size is sensible. * **Cron syntax follows Bunqueue 2.9 and Bun 1.4.** The dashboard accepts standard five-field expressions (including Bun's optional leading `+` on numeric values), six fields with leading seconds, and the official `@yearly`, `@annually`, `@monthly`, `@weekly`, `@daily`, `@midnight`, and `@hourly` shortcuts. Seven-field years and Croner-only `L`, `W`, `#`, and `?` extensions are rejected before submission. * **Upgrade old persisted schedules first.** A Croner-only definition created before Bunqueue 2.9 can make the broker fail closed at startup. Update or remove it while still running Bunqueue 2.8, then upgrade. * **Switches mirror the submitted value.** Overlap and missed-run defaults are initialized to the v2.9.3 defaults and every switch is sent explicitly, so an off switch cannot silently fall back to an on server default. * **You may reach this screen from more than one link.** An older, list-and-delete-only version of this page also exists. The sidebar's **Cron Jobs** entry always opens this full version. See [Known issues](/known-issues) for details. ::: details Under the hood (for developers) * Uses the `bq` client, not the legacy `api`. * **List:** `GET /crons` (a flat `{ ok, crons[] }` response), polled on the global refresh interval (default 3s, floored at 500ms), one request in flight at a time. * **Create:** `POST /crons` with `{ name, queue, data, schedule? | repeatEvery?, timezone?, priority?, maxLimit?, dedup?, jobOptions?, … }`. * **Delete:** `DELETE /crons/:name`. * A logically-failed create or delete (HTTP 200 with `ok: false`) surfaces as an error rather than a silent no-op. ::: --- --- url: /bunqueue-dashboard/docs/guide/flows.md description: >- Visualize a bunqueue job flow as an interactive DAG in the dashboard, parent, children, and dependency edges, coloured by state. --- # Flows The **Job Flows** page is both an interactive DAG explorer and an operator console for Bunqueue 2.9.4's official `FlowProducer` and Flow Job contracts. The URL preserves the loaded root, selected DAG node, and active Explore/Create/ Job methods tool. Back/Forward restores that context, while an invalid root or node is canonicalized before it can become a transport target. For BullMQ-compatible flow trees, children run before their parent; legacy `addChain`/`addTree` retain their documented execution direction. The page has three tools: * **Explore** draws the current parent/child/dependency topology and diagnoses missing backlinks, partial snapshots, cycles, and traversal caps. * **Create** executes `add`, `addBulk`, `addChain`, `addBulkThen`, or `addTree` with editable JSON and shows the atomic broker result. * **Job methods** exposes all safe remote methods: state predicates, broker-native `getFlow`, `toJSON`/`asJSON`, dependency/result reads, bounded `waitUntilFinished`, progress, log, delay, priority, log retention, deduplication, promote, and explicit dependency release. Payload replacement, retry, job removal and unprocessed-child removal are disabled and rejected by both the browser transport and the agent, including direct service calls. These SDK methods do not supply the atomic topology or generation preconditions missing from the corresponding HTTP operations. Delay/priority changes require a runnable job; promotion requires a delayed job. These reads do not create an atomic compare-and-swap guarantee. Creation and Job methods go through the local control agent to the exact TCP port of its managed server. A dashboard connected to another target fails closed instead of accidentally mutating the local broker. ## Broker-native tree and progress The **FlowProducer.getFlow** panel accepts a job ID, queue name, depth, and maximum children per level. Depth and child limits are explicit integers from 0 to 500. The result remains labelled with the target and limits captured when the request started, even if the form is edited while the request is in flight. It renders both a compact state-aware tree and the complete raw agent JSON; `flow: null` is shown as an explicit not-found result. `Job.updateProgress` follows both Bunqueue 2.9.3 forms. Numeric progress is bounded to 0-100 and may carry an optional message. Object progress is strict, bounded JSON and uses Bunqueue's canonical wire representation: numeric progress `0` with the serialized object in the progress-message field. A separate message with object progress is rejected because the upstream contract uses that same field for the object. Cycles, custom prototypes, unsafe property names, sparse arrays, non-finite numbers, non-JSON values, excessive nesting, and payloads above 65,536 UTF-8 bytes fail before broker access. ## Visual explorer The visual explorer deliberately builds its graph from the public HTTP job snapshots (`childrenIds`, `dependsOn`, `parentId`). This keeps links opened from Jobs/Job Inspector portable even when the local control agent is unavailable: 1. Paste any job ID, or open a job that is part of a flow in the [Job Inspector](/guide/job-inspector) and choose **View flow**. 2. The page climbs `parentId` to the flow's root, following up to the 100 levels supported by the v2.9.3 flow planner. 3. From the root it walks `childrenIds` (solid edges) and non-structural `dependsOn` links (dashed edges), up to 500 nodes. 4. It lays the graph out in columns by dependency depth and colours every node by state, including `waiting-children` for a parent blocked on its children. Job IDs must also be addressable by v2.9.3's non-decoding `/jobs/:id` HTTP route. Path-safe punctuation such as `:`, `@`, and `+` is preserved exactly; spaces, slashes, percent signs, and the exact dot segments `.` / `..` are rejected before a request is sent. The same validation is applied to parent, child, and dependency IDs returned by the server, so malformed topology cannot be normalized by the browser into a different URL. Every v2.9.3 job snapshot must explicitly contain all three topology fields: `parentId` as a string or `null`, plus `childrenIds` and `dependsOn` as arrays. An absent field is not interpreted as an empty relationship. A partial root or seed stops the traversal with an error; a partial referenced node is shown as unavailable with the malformed-field reason. ::: warning Destructive operations outside Flow Job methods Bunqueue v2.9.3 does not expose reverse-dependency inspection or topology-aware Cancel, Drain, Clean, Obliterate, DLQ Retry, or DLQ Purge. Deleting a referenced job can strand a parent in `waiting-children`, while a DLQ retry POST cannot atomically require the generation, state and topology seen by a prior GET; it can hit a different job recreated under the same ID. The dashboard therefore disables every manual, bulk and Copilot DLQ retry. Completed-job requeue is also disabled because `retryCompleted` does not reconstruct dependency registration or original flow order. Delete/purge paths fail closed, `maxAge`/`maxEntries` retention is read-only and omitted from DLQ policy saves, and auto-retry can only be disabled. ::: ## Runtime-only methods `extendLock`, `moveToCompleted`, `moveToFailed`, `moveToWait`, `moveToDelayed`, and `moveToWaitingChildren` belong to the worker that owns the active lease token. The dashboard never invents that token or turns those processor transitions into generic operator buttons. `discard()` is also process-local and non-awaitable. Use these inside the actual Worker processor; the page states this boundary beside the methods it can execute safely. v2.9.3 stores every canonical child in both the parent's `childrenIds` and `dependsOn`. The dashboard collapses that symmetric metadata into one solid parent-to-child edge; otherwise every normal relationship would be drawn twice in opposite directions and create an artificial cycle. Dashed edges therefore represent additional dependency links, such as one sibling waiting for another. Click any node to inspect it in the side panel, or open it in the Job Inspector. The whole graph is computed and rendered in the browser, with no graph library and no extra server endpoint. The page never hides an incomplete or corrupt graph. It reports traversal caps, missing/malformed job snapshots, inconsistent parent/child backlinks, and real dependency cycles next to the graph. Recently viewed roots are stored per Bunqueue server target, so switching connections cannot surface ids from a different server. When bearer authentication is active, recent roots remain in memory only and reset with the credentials; tokens are never persisted or used as storage keys. ## Try it The [live demo](https://egeominotti.github.io/bunqueue-dashboard/) ships a sample flow (an order that fans out to charge, ship, and notify, with a shipping-label child and a notify-depends-on-charge edge), so the page is populated out of the box. --- --- url: /bunqueue-dashboard/docs/guide/workflows.md description: >- Inspect Bunqueue Workflow Engine executions, durable decisions, signals, nested runs, and saga compensation. --- # Workflow Engine **Where:** use the dedicated **Workflow** sidebar section. * **Overview** (`/workflows`) is the command-center view. * **Job Flows** (`/flows`) renders parent/child and dependency DAGs. * **Executions** (`/workflows/executions`) browses the active store. * **Waiting & Signals** (`/workflows/waiting`) isolates parked executions. * **Compensation** (`/workflows/compensation`) combines `compensating` and `compensation-stuck` runs and exposes every per-step rollback outcome. * **Archive** (`/workflows/archive`) audits retained terminal executions. The Workflow Engine page reads Bunqueue 2.9.4's persisted execution contract from the configured SQLite `dataPath`. It shows active and archived executions, exact workflow/state filters, durable step progress, attempts, results, idempotency keys, loop occurrences, signals, branch decisions, definition identity, parent/child executions, pivot state, failure reasons, and every saga compensation outcome. The control agent opens SQLite read-only for observability and decodes the Engine's official structured-clone MessagePack blobs. List responses contain bounded summaries; the selected execution is decoded separately with explicit blob, depth, node, and string limits. An absent `workflow_executions` table is shown as an uninitialized store rather than as a broken page. Filters, active/archive store, page offset, selected execution, and the Summary/History/Payloads tab are encoded in the URL. Copying the address or using Back/Forward therefore restores the same operator context; incompatible or malformed parameters are removed before any request is issued. ## Connect the live Engine Workflow handlers are application code, so the dashboard never fabricates them or edits SQLite. Put their absolute module path in `BUNQUEUE_WORKFLOW_MODULE` under **Server → Extra environment**, then start the managed Bunqueue server. The module can export either: * `workflows`, an array of official Bunqueue `Workflow` definitions; or * `registerWorkflowRuntime(engine)` (or a default function) that registers the definitions and handlers on the supplied `Engine`. An optional `workflowNames` string array lets callback-style modules populate the start-execution suggestions. Registrations performed inside the callback are also discovered automatically. `BUNQUEUE_WORKFLOW_QUEUE_NAME` selects the Engine's internal step queue (default `__wf:steps`) and `BUNQUEUE_WORKFLOW_CONCURRENCY` sets worker concurrency from 1 to 1000 (default `5`). The runtime panel reports both effective values. The agent creates one persistent official `Engine` against the managed TCP port and `dataPath`, serializes commands, and closes it before server stop/restart. **Reload definitions** re-imports the module and replaces the Engine cleanly. ## Operator controls * **Overview** starts a named workflow with JSON input, recovers orphaned executions, reloads definitions, and shows the authoritative command result. * **Waiting & Signals** sends an exact durable event name and optional JSON payload to the selected waiting execution. * **Compensation** resumes a stuck unwind or explicitly abandons its remaining compensations after confirmation. * **Archive** can move `completed`/`failed` executions older than the chosen age into retention. Its separate cleanup action permanently deletes only eligible terminal rows still in the active execution store; it never deletes archive records, and the confirmation states that boundary explicitly. Every command is pinned to the Bunqueue target managed by the local agent. A remote/mismatched target, stopped server, missing module, malformed body, or non-terminal maintenance request fails closed. When `AGENT_TOKEN` is set, the normal agent bearer-token policy applies to every mutation. --- --- url: /bunqueue-dashboard/docs/guide/metrics.md description: >- A live, read-only dashboard that shows how well your server is keeping up right now: throughput, backlog trend, error rate, latency, and a per-queue… --- # Metrics A live, read-only dashboard that shows how well your server is keeping up right now: throughput, backlog trend, error rate, latency, and a per-queue breakdown. **Where:** open `/metrics` from the sidebar. ![Metrics](../screenshots/metrics.png) ## What you'll see The page updates on its own, there's nothing to click to refresh. At the top a **Live** dot tells you the connection is healthy. Below it, stat cards give you the headline numbers, two charts show trends over the last 60 seconds, and tables break down latency and per-queue counts. | Element | What it tells you | | --- | --- | | **Total Completed** | Retained completed jobs. Shown abbreviated (e.g. `5.8K`, `3.4M`), so it trades exact digits for readability. | | **Total Failed** | Current failed jobs summed across queues, shown as an exact count. | | **Push/sec** / **Pull/sec** | How many jobs per second are currently being added and taken off the queues. | | **Live Throughput** chart | Jobs per second over a rolling 60-second window, pushed, completed, and failed. The legend above it shows each series' current value. | | **Queue Depth** chart | Your backlog (waiting + prioritized + active + delayed + waiting-children jobs) over time, with a big current number and a trend label: `draining`, `steady`, or `accumulating`. | | **Error Rate** card | Failed jobs as a percentage of everything processed, plus the matching success rate and the exact completed/failed counts. | | **Server Overview** card | Current server-wide counts, queued, processing, delayed, dead-letter, totals pushed/pulled, and uptime. | | **Operation Latency** table | Network round-trip time per operation (`push`, `pull`, `ack`), average plus p50/p95/p99. | | **Per-Queue Metrics** table | One row per queue with its status and job counts (waiting, active, completed, failed). | ::: tip Watch the trend, not just the number On the Queue Depth chart, the trend label matters more than the raw figure. A backlog of 40,000 that's `draining` is fine; the same number `accumulating` means you're falling behind. The label turns green when draining and red when accumulating. ::: ## What you can do This screen is observe-only, there are no buttons that change server state, no forms, and no per-job actions. What you can do is read it: * **Watch throughput and depth live.** The charts and the header **Live** dot update automatically, roughly once a second. * **Read the depth trend** to judge whether you're keeping up, instead of eyeballing a single number. * **Page through the per-queue table** using the next/previous controls when you have more than 15 queues. * **Retry the connection.** If the server becomes unreachable, an offline banner with a **Retry** button appears under the header, click it to reconnect. ## Good to know * **Charts start empty.** The 60-second window builds up from the moment you open the page, there's no saved history. Leave it open for about a minute to fill the full window. * **A hidden tab freezes the charts.** When you switch away, updates pause and don't backfill. The series simply resume when you come back, so a real-world gap compresses into adjacent samples. * **Total Completed is abbreviated** (`5.8K`, `3.4M`) and loses exact digits. For precise completed and failed counts, read the footer of the **Error Rate** card. * **Latency is network time, not job time.** The Operation Latency table measures the round-trip for each `push`/`pull`/`ack` request, not how long jobs wait in the queue or take to run. The p99 column is tinted amber to draw your eye to tail latency. * **The Live dot requires both feeds.** The queue summary and live telemetry must both have data without a current error. Failures have separate banners; an unknown metric is displayed as a dash. * **The Error Rate turns red above 5%.** That's your at-a-glance signal that failures are climbing. ::: details Under the hood (for developers) * Live cards, both charts and the latency table are fed by a 1-second sampler that calls `GET /dashboard`. * The per-queue table comes from `GET /queues/summary`, polled on the global refresh interval (default 3s). * Both calls use the shape-verified `bq` client against the bunqueue HTTP API, not the control agent. Backlog depth (waiting + prioritized + active + delayed + waiting-children) is computed client-side each tick. * Earlier bugs are fixed here: the latency percentiles read real per-operation values, and uptime is scaled correctly. See [Known issues](/known-issues) for the remaining classic-page quirks. ::: --- --- url: /bunqueue-dashboard/docs/guide/workers.md description: >- A live registry of every worker connected to your server, including guarded cleanup for stale, idle registry records. --- # Workers A live registry of every worker connected to your server, so you can confirm your consumers are alive, see how much work each is doing, and clean up a stale registry record after stopping its process. **Where:** open `/workers` from the sidebar. ![Workers](../screenshots/workers.png) ## What you'll see A **Live** indicator next to the title tells you the page is getting fresh data; it drops off if the server stops answering. Five cards summarize the whole fleet, and a table lists each worker underneath. The summary cards: | Element | What it tells you | | --- | --- | | **Total** | How many workers are registered right now | | **Active** | Workers that are heartbeating (healthy) | | **Stale** | Workers that stopped heartbeating (turns amber when any exist) | | **Active Jobs** | Jobs being processed across the whole fleet | | **Quarantined** | Malformed registrations isolated from the actionable table | Each row in the table is one worker: | Column | What it tells you | | --- | --- | | **Worker** | The worker's name, with its full id below it | | **Queues** | The queues this worker consumes (`—` if none) | | **Status** | A pill: green **active** or amber **stale** | | **Active** | Jobs this worker is processing right now | | **Processed** | Jobs it has completed over its lifetime | | **Failed** | Jobs it has failed over its lifetime | | **Last Seen** | How long ago its last heartbeat arrived (e.g. "4s ago") | | **Actions** | Registry cleanup for a stale worker reporting zero active jobs | ## What you can do * **Remove a stale registry record**, click the trash icon on a stale row that reports zero active jobs. You'll be asked to confirm; on success a green message appears above the table, and the list refreshes. * **Retry**, if the server is unreachable, an offline banner appears with a **Retry** button to fetch again. Workers aren't created or edited here; they're started by your own consumer processes and register themselves. This screen is for monitoring them and cleaning up records only after the corresponding process has stopped. ::: warning Registry cleanup **does not stop the worker process**. In Bunqueue v2.9.3, a running worker does not automatically re-register after its heartbeat record is removed. Stop and verify the process first; use this action only for stale, idle records. ::: ## Good to know * **Quarantined records are not actionable.** Invalid identity, counters or status values produce a separate diagnostic list; they cannot be removed through the worker table. * **Stale doesn't mean stopped.** A worker that stops heartbeating turns amber and counts toward **Stale**, but its process may still exist. Verify it outside the dashboard before removing the record. * **The summary cards always reflect the full fleet.** Even when the table is capped, Total, Active, Stale, and Active Jobs are counted across every worker. * **The table shows at most 100 workers**, with no pagination. Past that, a note reads "Showing first 100 of N workers." See [Known issues](/known-issues). * **First load shows a brief "Loading workers…"**; after that, updates happen quietly in place with no flicker. If no workers are connected, you'll see an empty state instead. * **Don't confuse this with the classic Workers page** (`/workers-classic`), which is read-only, shows fewer rows, and has no status pill or registry-cleanup button. ::: details Under the hood (for developers) * Reads `GET /workers` via the `bq` client (payload wrapped in `data`); guarded registry cleanup calls `DELETE /workers/:id`. * Polls on the global refresh interval (default 3000 ms, adjustable in Settings, floored at 500 ms), pauses while the tab is hidden, and skips re-renders when the worker list is unchanged. * The response carries more fields than are shown (e.g. `concurrency`, `hostname`, `pid`, `registeredAt`, `uptime`); the table renders a subset. ::: --- --- url: /bunqueue-dashboard/docs/guide/logs.md description: 'Watch job activity across all your queues as it happens, in one live feed.' --- # Logs Watch job activity across all your queues as it happens, in one live feed. **Where:** open `/logs` from the sidebar. ![Logs](../screenshots/logs.png) ## What you'll see The page is titled **Activity Logs**. At the top, a **Live** indicator shows whether you're connected to the event stream right now. Below it sit six stat cards, a row of filters, and a table of recent events (10 per page). The stat cards count what has come through *since you opened the page*, they are running totals for this session, not all-time server figures. | Element | What it tells you | | --- | --- | | **Total Events** | Every job event received since you started watching. | | **Completed** | Events for jobs that finished successfully. | | **Failed** | Events for jobs that failed. Turns red when the count is above zero. | | **Waiting** | Events for jobs that were just queued and are waiting to run. | | **Active** | Events for jobs currently being picked up or making progress. | | **Throughput** | Current pace of events, in events per second, over the last 5 seconds. | Each row in the table is a single event: | Column | What it tells you | | --- | --- | | **Status** | A colored badge for the event's state (waiting, active, completed, failed). | | **Event** | The exact event type, such as `job:completed`, `job:pushed`, or `job:failed`. | | **Queue** | Which queue the event came from, or `—` if none was reported. | | **Timestamp** | How long ago the event happened, shown as relative time. | | **ID** | The job's ID, or `—` if it wasn't included. | ## What you can do This is a watch-only screen. Nothing here changes your server, the controls only narrow down what the feed shows you. * **Pick a queue.** Use the queue dropdown to focus on one queue, or choose **All Queues** to watch everything. Switching restarts the feed for the new scope (the counts start over). * **Filter by status.** Use the status buttons (all / waiting / active / completed / failed) to show only events in that state. * **Search.** Type into the search box to match by job ID or queue name. It's a plain, case-insensitive substring match. * **Page through events.** Use **Previous** / **Next** to move through the matching events, 10 at a time. ::: tip Changing the queue, status, or search jumps you back to the first page, so you always land on the newest matching events. ::: ## Good to know * **This is a live view, not a history log.** It keeps only the most recent 250 events and starts counting from the moment you opened the page. Reloading, navigating away, or switching the queue clears the feed and resets every counter. * **You can only search what's on screen.** Because the feed holds the last 250 events, older events that have scrolled off can't be searched or paged back to. * **Switching queues restarts the feed.** Moving between **All Queues** and a specific queue reconnects the stream, so the list clears and the rates briefly drop to zero before filling again. * **There's no job-name column.** Job events don't carry a name, so both log views show the event type and job ID instead. * **The empty table tells you what's happening.** You'll see **"Connecting to the event stream…"** while it connects, **"Waiting for activity…"** once connected but idle, and **"No events match the current filters."** when your filters hide everything. * **It reconnects on its own.** If the connection drops, the **Live** indicator goes off and the feed retries automatically after a couple of seconds. If the server is unreachable, a banner with a **Retry** button appears under the header. ::: details Under the hood (for developers) * The live feed reads a Server-Sent Events stream: `GET /events` for all queues, or `GET /events/queues/:q` when a queue is selected. Only frames whose event name starts with `job:` become rows; the buffer is capped at 250 events and flushed to the UI on a ~150 ms timer. * The stream is opened through the `api` client (URL + bearer auth), not a raw `EventSource`. * The queue dropdown's options come separately from `bq.queues()` (`GET /dashboard/queues`), polled every 30 s so it never competes with the live cadence. * A dropped stream retries after a 2 s backoff. ::: --- --- url: /bunqueue-dashboard/docs/guide/alerts.md description: >- Create browser-local threshold rules, inspect triggered alerts and understand delivery limits. --- # Alerts Open **Monitoring → Alerts** (`/alerts`) to evaluate thresholds against the connected Bunqueue server. Rules run in the browser while the dashboard tab is open, including when backgrounded. Closing the dashboard stops evaluation; this is not a server-side monitoring service. ## Create and verify a rule 1. Select **Create Alert Rule** and enter a descriptive name. 2. Choose the metric, comparison operator and threshold. Count metrics use whole jobs; error rate uses a percentage. P99 latency is global and cannot be scoped to a queue. 3. Optionally select a queue by entering its exact name, then select **Save rule**. 4. Check **Triggered Alerts** for the measured value, condition and queue. Enable or disable the rule in **Alert Rules**, or delete it after confirmation. For an isolated test queue containing one waiting job, a `Waiting >= 1` rule should trigger. The real browser test in `e2e/monitoring.e2e.ts` creates that job, saves the rule, observes the triggered row and removes the rule. See [Testing & verification](../testing.md). ## Read the state correctly * **Checking** means a current sample has not arrived yet. * A metrics error shows an unavailable/degraded state; it must not be treated as an all-clear. * **No triggered alerts** is meaningful only once enabled rules have current metrics. * Switching the connection invalidates results from the previous server. Rules are stored locally in the browser. Breaches produce in-app notifications and, when browser permission is granted, desktop notifications. Email, Slack and webhook delivery require an external monitoring service or hosted functionality; the local rule editor does not send them. --- --- url: /bunqueue-dashboard/docs/guide/server.md description: >- Server Control is where you start, stop, and restart the bunqueue server, set how it launches, and watch its logs live. --- # Server Control Server Control is where you start, stop, and restart the bunqueue server, set how it launches, and watch its logs live. **Where:** open `/server` from the sidebar. ![Server Control](../screenshots/server.png) ## What you'll see The page has four parts, top to bottom: a **Status console** at the top, a **Configuration** card, a **Storage** panel, and a live **Process logs** tail. The status console is the focal point, a mission-control readout of the running process. A colored dot tells you the state at a glance: green for running, amber while starting or stopping, red when stopped. Below it sits a cluster of vitals: | Element | What it tells you | | --- | --- | | **State** | `Running`, `Starting`, `Stopping`, or `Stopped`. A pulsing ring appears only when the server is running *and* healthy. | | **Version** | The bunqueue version the running server reports. | | **Health & uptime** | While running: `healthy` (or `waiting for health…`), the process id, and a live-ticking uptime. | | **Memory** | Current memory use in MB. Hover for heap detail. Shows `, ` while stopped. | | **Connections** | Live TCP, WebSocket, and SSE connection counts. Shows `, ` while stopped. | | **API endpoint** | The server's address. While running it's a clickable link to its health page, with a copy button. | | **Ports** | The HTTP and TCP ports in use. | | **Started** | When the process started (shown only while running). | | **Control agent** | The address of the local agent that manages the process. | | **Launch command** | The exact command the process was started with. | The **Configuration** card holds the settings the server will launch with next time you start or restart it: | Element | What it tells you | | --- | --- | | **Command** | The command the agent runs to launch bunqueue (default `bunx bunqueue@2.9.4 start`). | | **HTTP port** | The dashboard API and live-update port (1 to 65535). | | **TCP port** | The binary-protocol port. Must differ from the HTTP port. | | **Data path** | Where the SQLite database file lives. | | **Environment variables** | A key/value editor for any extra settings you want to pass in. | The **Storage** panel shows the SQLite database's footprint on disk as one proportional bar: the main **Database** file plus its **WAL** and **SHM** sidecars, with the total size, the file path (copyable), and when it was last written. Before the server has ever run, it shows a placeholder explaining the file appears after the first start. **Process logs** is a live tail of the server's output. Error lines show in red, agent messages in blue, normal output in muted gray. A footer shows the line count and whether the view is following the tail or paused. ## What you can do ### Attach-only mode Set `BUNQUEUE_MANAGED=0` when another supervisor owns the Bunqueue process. The status console is then labeled **External** and reflects the agent's authenticated `BUNQUEUE_URL/health` probe. A reachable degraded response is distinguished from an unreachable endpoint. Start, Stop, Restart, launch configuration, local storage statistics and child-process logs are hidden because they do not describe the external process; direct lifecycle/config requests also fail with HTTP 409. Backup restore is likewise unavailable: it requires proof that the database owner is stopped, which the attach-only agent cannot obtain from the supervisor. Unset the variable or set it to `1` to use the managed controls documented below. **Start the server.** Click **Start** and the agent launches bunqueue with your saved configuration. **Stop the server.** Click **Stop** to shut it down. ::: warning Stop asks you to confirm ("Stop the bunqueue server?") because it terminates the running process. ::: **Restart the server.** Click **Restart** to stop and start it again, the fastest way to apply configuration changes. This also asks for confirmation. **Edit and save the launch configuration:** 1. Change the command, ports, data path, or environment variables in the Configuration card. 2. Click **Save config** to store your changes for the next start (a "Saved" note flashes for a moment). 3. To apply them right away instead, click **Save & restart**, this saves *and* restarts in one step. It confirms first and only appears while the server is running. **Manage environment variables.** Add a variable with a key and value, remove one with the `✕` button, or click a preset chip (like `LOG_LEVEL` or `AUTH_TOKENS`) to add a common setting fast. These stay in the form until you save. **Work with the logs.** Filter by stream (`all` / `stdout` / `stderr` / `sys`), search the text, toggle **Follow** to auto-scroll (or turn it off to read back without being pulled to the bottom), toggle **Times** to show timestamps, **Copy** what's shown, or **Download** it as a `.log` file. ::: tip Ports are checked before anything is saved Both **Save config** and **Save & restart** validate your ports first: each must be a whole number from 1 to 65535, and the HTTP port must differ from the TCP port. If a port is invalid, nothing is saved and an error appears in red next to the buttons. ::: ## Good to know * **Configuration never applies in place.** Changes to the command, ports, or data path only take effect on the **next start or restart**, the running server keeps what it launched with. Use **Save & restart** to apply immediately. When your saved config is ahead of the running one, a "Restart to apply changes" hint appears next to the buttons. * **The default command resolves Bunqueue 2.9.4 through `bunx`.** For offline or source-checkout workflows, point **Command** at a local entry instead, for example `bun run /path/to/bunqueue/src/main.ts`. * **PostgreSQL multi-broker mode:** add `BUNQUEUE_STORAGE_DRIVER=postgres` and `BUNQUEUE_POSTGRES_URL=…` under Environment variables, plus the same `BUNQUEUE_POSTGRES_NAMESPACE` on every member. Create one Dashboard profile and paired control agent per broker; [Fleet](/guide/fleet) groups members by the credential-free `host:port/database` target and namespace and can operate each lifecycle independently. Keep **Data path** on a durable location if you use the Dashboard Workflow Engine: the agent retains it for Workflow state but removes every SQLite path alias from the PostgreSQL broker environment. Database inspection and S3 snapshots remain SQLite-only and return a clear unavailable response while PostgreSQL is active. Bunqueue 2.9.3 uses PostgreSQL schema 20 (the published 2.9.2 package used schema 19): upgrade every broker sharing the namespace together; an older broker cannot join after migration to schema 20. * **SQLite 2.9.3 migration:** make a copy of the database before the first start. Bunqueue upgrades it to schema 37 before binding the server and resumes checkpointed migrations after a restart. Do not try to downgrade a database after a partial or completed migration. * **Completed history has two separate controls.** `BUNQUEUE_MAX_COMPLETED_JOBS` caps the hot in-memory/recovery snapshot; it no longer deletes durable rows. Set `BUNQUEUE_COMPLETED_RETENTION_MS` only when durable completed jobs should expire by age. Both are available as Environment-variable presets in Server Control and take effect on restart. * **The data path's folder must already exist.** The server creates the database *file* but not its parent *folder*. A start that fails with a "cannot open" error usually means the directory isn't there yet, create it, or pick a path whose folder already exists. * **Logs don't keep forever.** Only the most recent ~800 lines are held, so older output scrolls off. Use **Download** to save a copy you want to keep. * **When the agent can't be reached**, the page tells you plainly. If it was never reachable, you'll see how to start it. If it stops responding after working, an amber banner shows the last known state and the Start/Stop/Restart buttons are disabled until it answers again, this is intentional, so the page never claims a dead server is "healthy." It reconnects on its own once the agent is back; no reload needed. See [Known issues](/known-issues). * **Memory and Connections need a running server.** Those vitals come from the live server, so they read `, ` whenever it's stopped. ::: details Under the hood (for developers) * Lifecycle, configuration, and logs all go through the local **control agent** (`bq.control.*`, default `http://localhost:6800`): `GET /control/status` is the primary poll, `POST /control/start|stop|restart` drive the process, `PUT /control/config` persists config to the agent's private settings file, and `GET /control/logs` feeds the log tail. * The live **Memory** and **Connections** vitals come from the bunqueue server's own `GET /health` (via `bq.health()`, called with `strict:false`) and are polled only while the process is running. * Polling uses the global refresh interval from Settings, **3000 ms by default** (floored at 500 ms), at most one request in flight. There is no SSE on this page; the uptime clock is a separate client-side 1-second ticker. ::: Saved server settings survive agent and dashboard restarts. The default file is `.bunqueue-dashboard/config.json` in the launch directory; service installations can select an absolute `AGENT_CONFIG_PATH`. Saved settings take precedence over initial environment defaults. Loading them does not start the server automatically. --- --- url: /bunqueue-dashboard/docs/guide/add-job.md description: >- Add Job lets you drop a job straight into any queue, one job or thousands of copies, without writing any code. --- # Add Job Add Job lets you drop a job straight into any queue, one job or thousands of copies, without writing any code. **Where:** open `/add-job` from the sidebar. ![Add Job](../screenshots/add-job.png) ## What you'll see The page is a single form with two cards and a submit row, no live counters or tables, just the fields you fill in. **Job** | Element | What it's for | | --- | --- | | **Queue** | The queue to add the job to. Start typing to pick from your existing queues, or enter a brand-new name to create one. | | **Data (JSON)** | The job's payload, written as JSON. Comes pre-filled with a small example you can replace. | **Options**, every field is optional. Leave a box empty and the server's default (shown as the placeholder) applies. | Element | What it's for | | --- | --- | | **Priority / Group priority** | Order in the queue. With a Group ID it becomes the 2.9.3 intra-group priority (`0` is highest, maximum `2,097,151`). | | **Delay (ms)** | Hold the job for this long before it can run. | | **Max attempts** | How many tries before the job is exhausted and sent to the dead-letter queue. | | **Backoff (ms)** | Wait time between retries. | | **Timeout (ms)** | Maximum time a single attempt may run. Blank means no limit. | | **Custom job ID** | Your own ID for the job. Reusing an ID prevents duplicates. | | **removeOnComplete** | Delete the job record once it finishes successfully. | | **removeOnFail** | Delete the job record once it fails for good. | | **durable** | Keep the job persisted. | | **lifo** | Add to the front of the queue instead of the back. | | **Tags / Group ID / Depends on / Unique key** | Group, dependency and deduplication metadata for advanced workflows. | | **Group max size** | Atomic cap on pending jobs in the selected group. Requires a Group ID; an enqueue beyond the cap is rejected. | | **Job name** | The first-class worker routing name. It is separate from the JSON payload and defaults to `default`. | | **Repeat policy (JSON)** | The safe v2.9.3 interval form: `{"every":60000,"limit":10}`. Create cron-expression schedules in **Cron Manager**. | **Submit row** | Element | What it's for | | --- | --- | | **Count** | How many copies of this job to add. Defaults to 1. | | **Add job** | Submits the form. A message appears next to it: green with the new job ID on success, red if something went wrong. | ::: tip The four toggles (removeOnComplete, removeOnFail, durable, lifo) all start off. Leaving one off means "use the server default", it isn't forced to false. ::: ## What you can do **Add one job.** Pick a queue, edit the JSON, adjust any options, and press **Add job**. On success the accepted ID or distinct-ID count is shown. **Add many jobs at once.** Set **Count** above 1 to enqueue that many copies of the same job in one go. The result line tells you how many were created. **Create a new queue on the fly.** Type a queue name that doesn't exist yet, the queue is created the moment you add the first job. **Fine-tune with options.** Fill in any option field to override its default; leave it blank to keep the default. Nothing is sent until you press **Add job**, and there's no confirmation step, the job goes in immediately. Before it sends, a few checks run: 1. **Queue** must not be empty. 2. **Data** must be valid JSON, any error shows in red under the editor. 3. **Count** must be a whole number of at least 1. 4. **Count** can be at most 10,000. 5. **Repeat policy**, when present, must contain a positive whole-number `every` value and may contain a positive whole-number `limit`. No other keys are accepted by this dashboard. ::: warning Do not send `repeat.pattern` through the v2.9.3 push route. That release stores the pattern but continues the repeat with `every ?? 0`, which can create an immediate hot loop instead of following the cron expression. Use **Cron Manager** for cron-expression schedules; it uses the dedicated `/crons` API. ::: ::: warning Typing a queue name that doesn't exist creates a brand-new queue. Double-check the name before a large bulk add, or a typo will scatter jobs into an unintended queue. ::: ## Good to know * **Name and ID are separate.** **Job name** classifies the work and defaults to `default`; the optional **Custom job ID** controls its caller-selected identity. User payload remains in JSON data. * **Group admission stays atomic.** Bunqueue 2.9.3's single HTTP add route does not forward `groupMaxSize`. When **Group max size** is filled, the Dashboard transparently sends a one-entry bulk request, which does preserve the field; it never silently submits an uncapped group job. * **Count copies are identical.** Every copy shares the exact same data and options. For different payloads, use **Bulk import**, which accepts JSON array/NDJSON job specs and preserves the operator-safe v2.9.3 fields: structured backoff, tags/groups/dependencies, interval repeat, dedup, stall timeout, stack-trace limit and timestamp, in addition to the single-add options. * **The complete bulk request is bounded.** Bunqueue limits data per job but not data multiplied by Count. The dashboard measures the exact translated JSON envelope without constructing it and refuses submissions above 64 MiB, so a valid large payload cannot exhaust the browser by being copied thousands of times. * **Bulk plus a custom ID collapses into one job.** If you set **Count** above 1 *and* a **Custom job ID**, every copy shares that ID, so the server dedupes them into a single job. The result line honestly reports how many distinct jobs were actually created, often just one. See [Known issues](/known-issues). * **Fire-and-forget.** This page reports the new job ID but doesn't track the job afterward. Use the Job Inspector or the Jobs page to watch it run. * **Rare bulk-only options live in Bulk import.** The friendly form exposes the single-push surface; spec mode additionally accepts `stallTimeout`, `dedup`, `stackTraceLimit` and `timestamp`. It rejects `parentId`, `childrenIds` and the dependency-failure flags because those belong to atomic Flow creation. It also rejects Bunqueue's persisted compatibility fields `keepLogs`, `sizeLimit`, `debounceId` and `debounceTtl`, which are not enforceable enqueue controls in v2.9.3. * **Autocomplete needs a connection.** Queue suggestions come from your live server. If it's unreachable the field still works as free text, you just won't get suggestions, and submitting shows the error in the result line. ::: details Under the hood (for developers) * Uses the **`bq`** client throughout (never `api.ts`). * Queue autocomplete: `GET /dashboard/queues`, polled every **30 s**. * Single add (Count = 1 without Group max size): `POST /queues/:q/jobs`. * Bulk add (Count > 1 or Group max size is set): `POST /queues/:q/jobs/bulk` with N copies of the body, after enforcing the 64 MiB aggregate UTF-8 envelope budget. * The client treats an HTTP 200 carrying `{ ok: false }` as an error, so logical failures surface in the red result line instead of being swallowed. ::: --- --- url: /bunqueue-dashboard/docs/guide/bulk-add.md description: >- Import distinct jobs from a JSON array, NDJSON, or a file into one Bunqueue queue. --- # Bulk Add Jobs Open **Bulk Add Jobs** at `/jobs/bulk-add`. Choose a queue and paste a JSON array or newline-delimited JSON, or load a file. This screen imports distinct jobs; use [Add Job](/guide/add-job) to submit one job or repeat a single payload. ## Import 1. Choose the input mode. **spec** accepts job options alongside `data`; **raw** treats each entry as the job's data. 2. Paste or load your input. The preview reports the parsed job count and any validation error before submission. 3. Select an existing queue or enter a new queue name. Set optional default priority, attempts, backoff and timeout values. 4. Submit and inspect the accepted count, then open [Jobs Explorer](/guide/jobs) for that queue to inspect the stored jobs. Example job specs: ```json [ { "name": "welcome", "data": { "recipient": "first" }, "priority": 1 }, { "name": "welcome", "data": { "recipient": "second" }, "delay": 5000 } ] ``` Per-job options override defaults. Malformed JSON, unsupported options, invalid numbers and unsafe dependency specifications are rejected before the request. Use [Job Flows](/guide/flows) to construct dependency graphs. ## Limits and results The form accepts at most **10,000 jobs** and applies **64 MiB** bounds to input bytes and the serialized request. A reverse proxy or server may impose a smaller request limit. Split large imports when needed. The result is based on the returned job IDs, not just HTTP success. Deduplication can reduce the number of distinct accepted IDs. The dashboard does not transparently retry imports: after an ambiguous network failure, inspect the queue before submitting again. The browser suite imports two distinct jobs through this page and independently reads their queue count from the real broker. Validation and response-contract tests cover malformed inputs, limits and stale server selections. See [Testing & verification](/testing) for the full scope. --- --- url: /bunqueue-dashboard/docs/guide/job-inspector.md description: >- Look up a job, inspect its data, result, errors, logs and history, and run only the mutations authorized by state and the v2.9.3 atomicity policy. --- # Job Inspector Look up any single job from one screen: inspect its data, result, error, logs and history, edit eligible fields, and run only the actions authorized by both its state and the v2.9.3 atomicity policy. **Where:** open `/job` from the sidebar. ![Job Inspector](../screenshots/job-inspector.png) ## What you'll see At the top there's always a **lookup bar**: a mode dropdown (**By job ID** / **By custom ID**), a search box, and a **Look up** button. Below it, a single status line shows the result of your last action, green for success, red for failure. Once a job is loaded, the screen splits into two: a wide left column with detail cards, and a narrow **Actions** rail on the right. The overview card at the top summarizes the job: | Element | What it tells you | | --- | --- | | Job ID | The job's internal ID, with a copy button. | | Queue | The queue this job belongs to. | | Status badge | The job's current state, color-coded. | | Priority | The job's priority number, shown as-is. | | Attempts | How many times it has run vs. its max (e.g. `0 / 3`). | | Progress | Reported progress, as a percentage. | | Created / Started / Completed | Local timestamps for each stage (blank until reached). | | Duration | How long the run took, once it has finished. | | Custom ID | Your own / idempotency ID, if the job has one (with a copy button). | The other cards appear depending on the job: | Card | What it shows | | --- | --- | | Data | The job's payload as formatted JSON; editable only while a non-Flow job is waiting, prioritized, or delayed. | | Result | The stored return value, only for **completed** jobs. | | Error | The last error message and full stack trace, only for **failed** jobs. | | Logs | The job's log lines, with controls to refresh, clear, and add lines. | | Child values | Resolved return values from a flow job's children, only for parent jobs. | | Timeline | The job's state history: enqueued, started, finished, and any retries. | | Backoff | A preview of when the remaining retries would run. | ## What you can do **Look up a job** 1. Pick **By job ID** or **By custom ID** in the dropdown. 2. Type the ID and press Enter (or click **Look up**). 3. The job loads and the page URL updates so you can bookmark or share a direct link to it. Every job ID elsewhere in the dashboard (Jobs, DLQ, Activity) links straight to this screen, so you rarely have to type an ID by hand. **Edit the payload** 1. Change the JSON in the **Data** card when **Save data** is available. 2. Click **Save data**. Valid JSON is saved and the job reloads; invalid JSON shows an inline message and nothing is sent. Flow members are always read-only. Bunqueue v2.9.3 replaces the entire payload on update, while FlowProducer stores parent/child topology in reserved payload keys; allowing an ordinary edit would make the flow unreadable. Jobs that have started processing or left the runnable queue are read-only as well. **Run an action**, the Actions rail only shows the actions that are valid for the job's current state. Depending on state, you may see: | Action | What it does | | --- | --- | | **Promote (run now)** | Pulls a delayed job forward to run immediately. | | **Set priority** | Sets a new priority number. | | **Set delay** | Sets a new delay in milliseconds. | | **Set progress** | Updates progress for an active job without moving its state. | A failed job instead shows an unavailable-DLQ-retry notice. A completed job shows an unavailable-requeue notice; neither state exposes a mutation button. **Work with logs**, use **Refresh** to reload the lines, type a message and pick a level (`info` / `warn` / `error`) then **Add** to append one, or **Clear logs** to wipe them all. **See child values**, on a flow parent, click **Show** to load and view the resolved return values of its children. ::: warning Unsafe state changes fail closed Bunqueue cannot stop worker code already processing an active job, so the inspector offers only progress there. Cancel/delete/discard is unavailable in every state because v2.9.3 has no reverse-dependency check or expected-state/flow-atomic Discard operation. DLQ retry is unavailable because the separate GET + POST has no atomic generation/state/topology precondition; completed requeue is unavailable because `retryCompleted` does not restore dependency registration/flow order. **Clear logs** remains destructive and asks for confirmation. ::: ## Good to know * **Actions are state-aware and contract-aware.** Only delayed jobs can be promoted, only active jobs expose progress, and only runnable queued states expose priority/delay. Failed and completed jobs expose policy notices, not retry/requeue controls. * **Flow data is protected.** The inspector detects public parent/children links and Bunqueue's reserved flow keys and never offers the full-payload data write for those jobs. * **Backoff times are approximate.** The retry schedule is a preview and doesn't include the random jitter the server adds at retry time (up to ±50%, or ±20% for fixed backoff), so read the numbers as "about". "exponential (default)" just means the job uses standard backoff, not that it has none. * **Timeline keeps the last 20 entries.** Very retry-heavy jobs only show their most recent transitions; older attempts (and the errors attached to them) drop off. * **Completed-job requeue is not available.** The upstream operation resets a run without reconstructing dependency registration and original flow order. * **The result is fetched on demand.** A completed job with nothing stored shows "No result stored for this job." rather than an error. * **Rapid lookups are safe.** If you hammer Enter, the newest lookup always wins, a slow earlier response can't overwrite it. * **This is the modern inspector.** The classic Jobs and DLQ views have separate, documented quirks. If something looks off, check [Known issues](/known-issues). ::: details Under the hood (for developers) * Uses the shape-verified **`bq`** client throughout (never the legacy `api`). * Lookup calls `GET /jobs/:id` or `GET /jobs/custom/:customId`; a completed job also fetches `GET /jobs/:id/result`. Logs use `GET/POST/DELETE /jobs/:id/logs`; children use `GET /jobs/:id/children` (lazily, on expand). * Enabled state changes map to `POST /jobs/:id/promote`, `PUT /jobs/:id/data | priority | delay` and `POST /jobs/:id/progress`; `/data` is exposed only for runnable non-Flow jobs. Delete, Discard, active move/fail/retry, every DLQ retry and completed-job requeue are intentionally absent. * **No polling or SSE.** It fetches once per lookup, then re-fetches only after an action, a Logs refresh, or expanding child values. Deep links (`/job?id=`) auto-load on open. ::: --- --- url: /bunqueue-dashboard/docs/guide/queue-control.md description: >- Your operations console for a single queue: pause, promote, set explicit policies, and tune it with flow-unsafe deletion paths disabled. --- # Queue Control Your operations console for a single queue: pause, promote, set explicit policies, and tune it with flow-unsafe deletion paths disabled. **Where:** open `/queue-control` from the sidebar. ![Queue Control](../screenshots/queue-control.png) ## What you'll see Start with the **queue picker** at the top and choose a queue by name. Next to it, a colored **status dot** shows whether that queue is running, and an inline **message** reports the result of your most recent action. The first queue is selected for you on load. Once a queue is selected, a row of eight count cards summarizes every v2.9.3 job state (numbers shown with thousands separators), followed by cards for every control you can operate. | Element | What it tells you | |---------|-------------------| | Status dot | Green **Active** when running, amber **Paused** when the queue is paused. | | Last-action message | Green on success (with an affected count when available), red with the error text on failure. | | **Waiting** | Jobs queued and ready to run. | | **Prioritized** | Ready jobs held in the priority-ordered queue. | | **Active** | Jobs being processed right now. | | **Completed** | Jobs that finished successfully. | | **Failed** | Jobs that ran out of retries (dead-lettered). | | **Delayed** | Jobs scheduled to run later. | | **Waiting-children** | Flow parents blocked on structural or dependency children. | | **Paused** | Jobs held because the queue is paused. | Below the counts you get the **Lifecycle** card (pause, promote, and visibly unavailable requeue/deletion controls), the **Rate-limit desired state** and **Concurrency desired state** cards, a live **Queue SDK operations** console, and the **Stall detection** and **DLQ policy** forms. ## What you can do **Pause / Resume**, one button toggles the queue between running and paused; its label and color follow the current state. **Requeue completed** is visible but disabled. Bunqueue v2.9.3's `retryCompleted` path resets the job without reconstructing dependency registration or the ordering guarantees of its original flow. **Promote delayed**, moves delayed jobs to waiting so they run now. Leave the **Promote** box empty to promote all of them, or enter a number to promote just the first *N*. **Replace rate-limit policy**, enter a positive integer limit and an explicit positive **Window (ms)**, then choose a permanent policy or **Expires after** with a TTL. The HTTP mutation remains an explicit desired-state replacement; the separate SDK readback below shows the authoritative current policy. **Replace concurrency policy**, enter a positive integer maximum in-flight count. Clearing either policy is labelled **Ensure no …**, requires typing the exact queue name, and refreshes the independent SDK readback after the server acknowledges the mutation. **Inspect Queue SDK state**, refresh the official global rate-limit, concurrency, remaining rate-limit TTL, and saturation contracts. The same console reads a group's counts, pause state, bounded pending-job page and per-priority counts; pauses/resumes that group and sets/clears its rate and concurrency policies. It also resolves or explicitly releases a deduplication key, reads paged completed/failed one-minute metric buckets, and trims the bounded lifecycle event journal after confirmation. Changing queue clears every snapshot and receipt immediately. Changing connection profile also invalidates in-flight reads and post-mutation refreshes, so a result from one broker is never relabelled or continued against another. To adjust stall detection: 1. Open the **Stall detection** form. 2. Toggle **enabled**, then fill in **Stall interval (ms)**, **Max stalls**, and **Grace period (ms)**, all three are required. 3. Click **Save**. You'll see `Saved ✓` when it lands. To adjust the dead-letter policy: 1. Open the **DLQ policy** form. 2. If upstream auto-retry is already enabled, turn it off. The dashboard never permits enabling it because v2.9.3 cannot verify hidden reverse flow dependencies. Retry interval and max-auto-retries remain editable. 3. Read **Max age** and **Max entries** as server state only. They are disabled: lowering `maxEntries` can immediately evacuate entries, and `maxAge` drives destructive expiry without an atomic generation/topology check. 4. Click **Save**. The request deliberately omits `maxAge` and `maxEntries`. ::: warning Flow-destructive operations fail closed **Drain** and **Clean** are visible but disabled. v2.9.3 has no reverse-dependency lookup or atomic topology mutation, so no queue scan or confirmation can prove that deleting those jobs will not strand a cross-queue parent. **Obliterate**, job Cancel and DLQ Purge follow the same policy. Every DLQ retry also fails closed because its POST has no atomic generation/state/topology precondition; completed-job requeue cannot restore flow dependency registration/order. ::: ## Good to know * **Switching queues discards unsaved edits.** If you type into the Stall or DLQ form and change queues before saving, your changes are lost. * **A live update can overwrite your edits.** If the same queue's config changes elsewhere while you're editing, the form may refresh to the new values. Typing is otherwise preserved across background refreshes. * **Numeric policy fields are validated before sending.** Stall fields and the editable DLQ retry interval/count require non-negative safe integers. Rate-limit and concurrency require positive safe integers; the rate window is mandatory and TTL is an explicit permanent/expiry choice. DLQ retention values are never parsed into a save because they are read-only. * **Some controls appear only when the server supports them.** If a queue has no stall or DLQ configuration, that form is hidden for it. * **When the queue can't be reached**, a banner with a **Retry** button appears above the content. While an action is running, the buttons are briefly disabled. * The last-action message is a single shared line, each new action replaces the previous result. ::: details Under the hood (for developers) This screen uses `bq` for HTTP controls and a repository adapter for the pinned agent Queue SDK bridge. The queue picker polls `GET /dashboard/queues` every 30 s. The selected queue refreshes on the global live cadence (default 3 s, configurable in Settings, floor 500 ms), fetching counts + paused state (`GET /dashboard/queues/?includeJobs=false`) alongside `GET /queues//stall-config` and `.../dlq-config`. Enabled actions map to: `POST .../pause` · `.../resume` · `.../promote-jobs`; `PUT`/`DELETE .../rate-limit` (body `{ limit, duration, ttl? }`) and `.../concurrency` (body `{ concurrency }`); and `PUT .../stall-config` · `.../dlq-config`. The DLQ save projects only `autoRetry`, `autoRetryInterval` and `maxAutoRetries`; it never sends `maxAge` or `maxEntries`, and only `autoRetry:false` is permitted. The upstream retry-completed/drain/clean routes exist but are intentionally never called here. The client throws on any HTTP-200 response with `{ ok: false }`, so logical failures surface as the red inline error. SDK reads and mutations use `/agent/queue-operations/:queue/*`, are pinned to the process manager's running port, validate exact bounded input, serialize access, and close their dedicated Bunqueue `Queue` connection after each operation. ::: --- --- url: /bunqueue-dashboard/docs/guide/dlq-control.md description: >- Pick one queue to inspect and export failed jobs; all retry and purge mutations fail closed under the v2.9.3 contract. --- # DLQ Control Pick one queue to inspect and export the jobs that failed for good. All row, bulk and queue-wide retry mutations are unavailable under the v2.9.3 fail-closed policy. **Where:** open `/dlq-control` from the sidebar. ![DLQ Control](../screenshots/dlq-control.png) ## What you'll see The header reads **Dead Letter Queue** with a **live** badge, meaning the table refreshes on its own. Below it is a queue picker and a summary card, then the table of failed jobs. | Element | What it tells you | | --- | --- | | **Queue** dropdown | Which queue you're looking at. Each option shows its name and, in parentheses, how many jobs are stuck in its DLQ (e.g. `image-resize (3)`). Queues with none show just the name. | | **Entries** card | The total number of dead-lettered jobs in the selected queue. It turns **red** when there are any, and stays neutral at `0`. | | **Job ID** | The failed job's identifier. | | **Reason** | Why the job was dead-lettered, shown as a red badge (for example, `max_attempts_exceeded`). | | **Error** | The last error message the job hit. Shows `, ` when there's nothing to display. | | **Attempts** | How many times the job ran before giving up. | | **Entered** | When the job landed in the DLQ, as relative time (e.g. "12m ago"). | When there are more than 25 jobs, use the **pagination** control at the bottom to move through the pages. ## What you can do * **Switch queue**, pick a different queue from the dropdown to load its DLQ. The table jumps back to the first page. On first open, the screen automatically selects the first queue that actually has failed jobs. * **Export**, download the currently displayed entries as CSV. * **Retry one job**, **Retry all** and **Purge** are visible but permanently disabled. The retry POST cannot atomically require the generation, state and topology observed by a prior GET; purge cannot inspect hidden reverse dependencies. The disabled controls explain the policy in their tooltips and send no mutation request. Export is a browser-side download and does not change the queue. ::: warning An exact ID is not an atomic identity Between `GET /jobs/:id` and `POST /queues/:q/dlq/retry`, the observed job can be removed and a new job created under the same ID. Bunqueue v2.9.3 gives the POST no generation/state/topology precondition, so even an exact, fresh, topology-empty snapshot cannot make row retry safe. ::: ## Good to know * **Individual removal is not exposed; Retry all and Purge stay disabled** for every queue and page. * **If the DLQ is empty**, you'll see "Dead letter queue is empty" and the **Entries** card reads `0`. * **The dropdown count and the Entries card update on slightly different clocks**, so after an external mutation or server-side retention event the number in parentheses may briefly lag behind the card. Give it a moment and they'll line up. * **If the server can't be reached**, a banner with a **Retry** button appears and the last loaded rows stay on screen so you don't lose your place. * **This is the focused, single-queue view.** For a cross-queue DLQ with filters, use the DLQ Pro screen instead. Avoid the older off-menu classic DLQ page, which is known to break on non-empty queues, see [Known issues](/known-issues). ::: details Under the hood (for developers) * Every request uses the `bq` client against the bunqueue HTTP API, never the legacy `api` layer. * Queue list: `GET /dashboard/queues`, polled every **30 s** (this feeds the dropdown counts). * Table: `GET /queues/:q/dlq?limit=25&offset=…`, polled at the connection store's global cadence (**default 3 s**, floored at 500 ms). Response is flat, `{ ok, entries[], total }`, no `data` wrapper. * The page never calls a DLQ retry, removal or purge route; all corresponding controls are disabled because no upstream mutation has an atomic generation and topology precondition. * A DLQ entry is `{ job, enteredAt, reason, error, attempts[] }`, the id and attempt count live nested under `job`, with no top-level `id`. ::: --- --- url: /bunqueue-dashboard/docs/guide/webhooks.md description: >- Register HTTP callbacks so bunqueue pushes job events to your own endpoint the moment they happen, no polling on your side. --- # Webhooks Register HTTP callbacks so bunqueue pushes job events to your own endpoint the moment they happen, no polling on your side. **Where:** open `/webhooks` from the sidebar. ![Webhooks](../screenshots/webhooks.png) ## What you'll see A header with a **Live** indicator (the page refreshes on its own), an **Add webhook** card that's always available, and a table of the callbacks you've registered. Each row is one webhook. | Element | What it tells you | | --- | --- | | **URL** | Where bunqueue POSTs the event. Long URLs are trimmed to fit, hover to see the rest. | | **Events** | Which job events this hook is subscribed to (for example `job.completed, job.failed`). | | **Queue** | The queue it watches. **`all`** means it fires for every queue. | | **Success / Fail** | How many deliveries have succeeded (green) or failed (red when non-zero). Running totals. | | **Last** | When the hook last fired, shown as relative time (for example "20s ago"). | | **Enabled** | A switch showing whether deliveries are currently active. | ::: tip Reading the counters A climbing **Fail** count with **Success** stuck at 0 usually means the receiving endpoint is unreachable or rejecting every call. Bunqueue performs delivery retries; the dashboard displays the counters and does not provide manual redelivery. Check the receiving endpoint and the broker logs. ::: ## What you can do **Add a webhook.** Fill in the **Add webhook** card and submit: 1. Enter a **URL** (required). 2. Optionally set a **Queue**, leave it blank to watch every queue. 3. Optionally set a **Secret**, an HMAC signing secret to verify calls came from bunqueue. 4. Pick one or more **event pills** (at least one is required). `job.completed` and `job.failed` are selected by default; available events are `job.pushed`, `job.started`, `job.completed`, `job.failed`, and `job.progress`. 5. Click **Add**. The button shows "Adding…" while it works, then the new hook appears in the table. If the URL is empty or no events are picked, you'll see "URL and at least one event are required" and nothing is sent. After a successful add, the URL and Secret fields clear while your queue and event choices stay, handy for adding a similar hook. **Enable or disable a hook.** Flip the **Enabled** switch on any row. Deliveries stop or resume immediately; the registration and its counters are kept either way. **Remove a hook.** Click the trash icon on its row. ::: warning Removing is permanent Deleting a webhook asks you to confirm ("Remove webhook for \?"), then removes it along with its counters. There's no undo, you'd have to add it again. ::: ## Good to know * **There's no edit.** To change a hook's URL, events, queue, or secret, delete it and add a new one. * **The secret is write-only.** You can set a signing secret, but the page never shows it back to you. * **Delivery retries belong to Bunqueue.** Version 2.9.4 defaults to 3 retries with a 1,000 ms base delay (`WEBHOOK_MAX_RETRIES`, `WEBHOOK_RETRY_DELAY_MS`); the delay grows between attempts. The dashboard has no manual resend action. * **Endpoint safety:** Bunqueue rejects loopback and private network callback URLs. Local browser tests exercise registration, enable/disable and deletion with an unused queue and a reserved `.invalid` URL; they do not prove outbound delivery or signature verification. * **Empty and offline states are clear.** With no hooks yet, you'll see "No webhooks" and a prompt to add one. If the connection drops, the last table stays on screen with a **Retry** button, and a failed enable/disable/delete shows a red banner instead of silently snapping back. * **The list pages at 15 rows.** Large lists are split into pages of 15 in your browser, fine for typical use. See [Known issues](/known-issues) for notes on client-side pagination. ::: details Under the hood (for developers) This screen uses the bunqueue HTTP API on `:6790` (the `bq` client), not the control agent: * List: `GET /webhooks`, the payload is wrapped in `{ ok, data: { webhooks, stats } }`; `stats` is returned but unused. * Create: `POST /webhooks` with `{ url, events, queue?, secret? }`. * Enable/disable: `PUT /webhooks/:id/enabled` with `{ enabled }`. * Delete: `DELETE /webhooks/:id`. The list is polled on the global refresh interval (default 3000 ms, configurable in Settings), pauses while the tab is hidden, and refetches after every change. There is no SSE stream on this page. ::: --- --- url: /bunqueue-dashboard/docs/guide/diagnostics.md description: >- A single-glance health check for your bunqueue server, is it up, how long has it been running, is the disk full, what's connected, and how much memory is… --- # Diagnostics A single-glance health check for your bunqueue server, is it up, how long has it been running, is the disk full, what's connected, and how much memory is it using. **Where:** open `/diagnostics` from the sidebar. ![Diagnostics](../screenshots/diagnostics.png) ## What you'll see The page opens with a row of status cards across the top, two cards below it (**Connectivity** and **Memory**), and a **Totals since restart** card at the bottom. Everything updates on its own, this is your first stop whenever the dashboard feels off. ### Status row | Element | What it tells you | | --- | --- | | **Status** | The server's overall health. Green when healthy, red when `degraded`. | | **Version** | The bunqueue server version (for example `v2.9.3`), or `—` if the server doesn't report one. | | **Uptime** | How long the server has been running (for example `52m`). | | **Disk** | `Healthy` (green) normally, or `Full` (red) when the server is out of disk space. | ### Connectivity | Element | What it tells you | | --- | --- | | **WebSocket clients** | How many live WebSocket clients are attached to the server. | | **SSE clients** | How many live Server-Sent-Events subscribers are attached. The dashboard's own live activity stream counts as one, so `1` is normal while the dashboard is open. | | **Storage error** | The server's last storage error, or `none` when everything is fine. | ### Memory | Element | What it tells you | | --- | --- | | **Heap used** | Memory the server is actively using (for example `75.0 MB`). | | **Heap total** | Memory the server has reserved for its heap. | | **RSS** | Total resident memory the server process is holding. | ### Totals since restart Process-session counters since the server restarted: **Pushed** (jobs enqueued), **Pulled** (jobs handed to workers), **Completed** (jobs finished), and **Failed**. This card only appears when the server reports these totals. ## What you can do Most panels inspect the server. Manual actions include memory operations: * **Ping** (in the Connectivity card header), measures the round-trip time to the server and shows the result on the button (for example `Ping · 34 ms`, or `Ping · unreachable` if it can't reach the server). The result stays until you ping again. * **Compact (GC)** requests garbage collection and reports the before/after RSS difference, or that no memory was reclaimed. This affects the server process. * **Load / Refresh** in **Heap statistics** requests object counts and top object types; the upstream endpoint forces GC before collecting them. * **Copy** in **Prometheus** copies the current scrape URL. * **Retry**, appears on the offline banner when the server can't be reached. Click it to re-check the server right away. ::: tip Ping latency is measured from your browser, so it includes the full network path between you and the server, treat it as an end-to-end reachability check, not the server's internal processing time. ::: ## Good to know * **An SSE count of `1` is not a leak.** The dashboard's own live activity stream registers as one SSE client, so expect at least `1` while the dashboard tab is open. * **A degraded server still shows here.** If the server is unhealthy (for example the disk is full), the **Status** and **Disk** cards turn red but the page stays live and keeps updating, you won't lose visibility. * **Partial failures stay visible.** Each endpoint is captured independently. Missing disk data reads `Unavailable`, storage errors show their failure, and unavailable totals are omitted. The offline banner names partial refresh failures; unknown data is not labelled healthy. * **Orchestrator probes** display `/healthz`, `/live` and `/ready` independently. **JSON metrics** displays the `/metrics` response; the Prometheus text endpoint is a separate scrape target. ::: details Under the hood (for developers) * Polls `GET /health`, `/storage`, `/stats`, `/healthz`, `/live`, `/ready` and `/metrics` together at the global refresh interval (default **3000 ms**). Each response or failure is retained independently. * The **Ping** button hits `GET /ping` on demand only and reports the browser-measured round-trip time; it is not part of the poll loop. * Memory values are reported in MB and uptime in seconds; the page formats them for display. ::: --- --- url: /bunqueue-dashboard/docs/guide/benchmark.md description: >- Push a controlled flood of jobs at your bunqueue server and watch, in real time, how fast it accepts and clears them. --- # Benchmark Push a controlled flood of jobs at your bunqueue server and watch, in real time, how fast it accepts and clears them. **Where:** open `/benchmark` from the sidebar. ![Benchmark](../screenshots/benchmark.png) ## What you'll see On the left is a **Configuration** card where you set up the test. On the right, a live run panel shows progress bars, stat cards, and a **Throughput** chart. Below, once a run finishes, you get a **Summary**, the server's own **Server queue** counts, and a **Run history** table of past runs. The heading follows the run: `Ready`, then `Producing…` / `Running…`, `Draining…`, `Stopping…`, and finally `Result` (or `Error`). A small `ETA` next to it estimates the time left while the test is going. The live stat cards update several times a second: | Element | What it tells you | | --- | --- | | **Pushed** | Jobs successfully sent to the server so far. | | **Completed** | Jobs pulled and finished by the simulated workers. | | **Push/sec** | Current send rate, right now. | | **Done/sec** | Current completion rate, right now. | | **Elapsed** | Time since the run started. | | **Active workers** | How many worker loops are mid-cycle at this instant. | | **Data** | Total payload sent (bytes). | | **Errors** | Failed sends or acks; turns red if any occur. | The **Throughput** chart plots the last stretch of the run: send rate (cyan) against completion rate (green), so you can see the server keeping up, or falling behind. The **Server queue** card is the ground truth. It reads the server's own counts for your target queue, **Waiting**, **Active**, **Completed**, **Failed**, **Delayed**, every second, independent of the client-side numbers. It's your proof the load actually landed. ## What you can do **Pick a preset.** Smoke, Standard, Stress, and Soak each drop a ready-made configuration into the form. Smoke is a tiny 200-job sanity check; Standard is 5,000 jobs; Stress is a heavy 50,000; Soak runs for a fixed 30 seconds instead of a fixed count. Tweak any field afterward. **Configure a run.** The page assigns a cryptographically random, read-only **Dedicated queue** to this browser tab. Choose **Mode** (`count` for a fixed number of jobs, `duration` for a fixed time), then dial in producers, workers, batch sizes, payload size, and simulated processing time. Toggle **Durable** to fsync each job, and **Remove on complete** to drop finished jobs server-side. **Run a benchmark:** 1. Set your configuration (or pick a preset). 2. Click **Run benchmark**. The dashboard checks the server is reachable and, when simulated workers are enabled, verifies the target queue has no runnable jobs before it starts. 3. Watch the live panel; the chart and stat cards update as the load lands. 4. When it finishes, review the **Summary** and the run is added to **Run history**. **Stop early.** Click **Stop** at any time. In-flight work settles cleanly and you still get a Summary for what ran. **Clean up afterward.** Use **Clean queue** to purge leftover benchmark jobs from the queue. ::: warning Clean queue is destructive It permanently removes waiting, completed, failed, and delayed jobs from the target queue. You'll be asked to confirm (`Remove benchmark jobs from ""?`) before anything is deleted. ::: **Keep your results.** In **Run history**, use **Copy** or **Export JSON** to save your runs, or **Clear** to empty the table. ## Good to know * **Throughput is measured in your browser.** Push/sec and Done/sec reflect your machine, network, and browser as much as the server. When in doubt, trust the **Server queue** card over the live counters. * **The Summary's latency numbers cover sending only.** The avg / p50 / p95 / p99 / max figures measure how long each batch took to send, not how long workers took to process. * **Run history is temporary.** It keeps the last 12 runs and is lost on reload. Export JSON first if you want to compare later. * **Everything locks during a run.** All configuration fields, the toggles, the presets, and Clean queue are disabled while a benchmark is active. They free up the moment it ends or you Stop. * **Worker benchmarks require an empty queue.** Every generated id is tracked and only jobs belonging to the current run are acknowledged or counted. If an external producer races the run, its jobs are returned to waiting and the run stops instead of consuming that work. * **Clean queue can't remove active jobs.** Jobs that were pulled but not yet acked can't be cleaned; the server re-queues them itself after its stall timeout. You'll see a note like `Cleaned, N active job(s) remain`. * **This is real load with real cost.** Stress and Soak runs genuinely write to the server (and fsync if Durable is on). Point it at a disposable or dev server, and use **Clean queue** or **Remove on complete** so you don't leave a large backlog behind. * **If the server is unreachable**, the run won't start, you'll get a clear error, and the Server queue card shows a `stale` badge while keeping the last good counts. See [Known issues](/known-issues) for verified limits. ::: details Under the hood (for developers) This screen uses the `bq` client exclusively (never `api`). Per worker run it calls `GET /dashboard` and `GET /queues/:q/counts` as preflight checks, then `POST /queues/:q/jobs/bulk` for each producer batch, `POST /queues/:q/jobs/pull-batch` and `POST /jobs/ack-batch` for the simulated workers, and `POST /queues/:q/clean` for Clean queue. A foreign id is returned with `POST /jobs/:id/move-to-wait` and stops the run. The **Server queue** card polls `GET /queues/:q/counts` every 1 s. There is no SSE stream: a separate 200 ms client-side sampler derives the per-second rates and the rolling throughput series. Logical failures (`HTTP 200 + {ok:false}`) are counted as errors, with the first message surfaced in the summary. ::: --- --- url: /bunqueue-dashboard/docs/guide/database.md description: >- Browse and inspect your bunqueue server's underlying SQLite database, read-only, with no risk of changing anything. --- # Database Browse and inspect your bunqueue server's underlying SQLite database, read-only, with no risk of changing anything. **Where:** open `/database` from the sidebar. ![Database](../screenshots/database.png) ## What you'll see The page opens on a set of stat cards summarising the database, a list of tables on the left, and a data grid on the right. A **read-only** badge in the header is literal: nothing on this screen can change your data. The stat cards across the top tell you: | Element | What it tells you | | --- | --- | | **SQLite** | The database engine version. | | **On disk** | Total size on disk, including the write-ahead log. | | **Journal** | The journal mode (usually `WAL`). | | **Tables** | How many tables the database has. | | **Indexes** | How many indexes exist across those tables. | The **Tables** list on the left shows every table with its live row count; the one you're viewing is highlighted. Pick a table and the right side fills in. For the selected table you get two tabs: * **Data**, a sortable, paginated grid (50 rows per page). Primary-key columns carry a **PK** badge and each column shows its type. `NULL` values are shown faint and italic. A small amber `…` chip marks a value that was shortened to fit, click the row to read it in full. * **Schema**, the table's columns and their constraints, its indexes, and the original `CREATE TABLE` statement (with a copy button). At the bottom, a **Query** panel lets you run your own read-only SQL and see the results in the same kind of grid. ## What you can do * **Pick a table** from the left list to load its rows. * **Switch between Data and Schema** with the tabs above the grid. * **Sort a column** by clicking its header, it cycles ascending, descending, then off. * **Filter rows** using the filter bar: choose a column, an operator (`contains`, `=`, `≠`), type a value, and press Enter or **Filter**. Use **Clear** to remove it. (The **Filter** button stays disabled until you type a value.) * **Open a row** with its **View** button. A drawer slides in with every column's full, untruncated value and a copy button on each. Close it with the X, the backdrop, or `Escape`. * **Export the current page** as a CSV file, or **Export table** to download one bounded, point-in-time CSV of the filtered and sorted table view. Running your own query: 1. Type SQL into the **Query** box, or click **Query** on a table to pre-fill a `SELECT` for it. 2. Press **Run** (or `⌘/Ctrl+Enter`) to see the results, or **Explain** to see the query plan. 3. Reuse a recent query from the **History** chips, and download results with **CSV** or **JSON**. ::: tip Nothing here is destructive Every action is a read or a download, so nothing asks for confirmation. Even hand-typed SQL can only read: writes are rejected before they reach the database. ::: ## Good to know * **It's a viewer, not a console.** Only read queries run, anything that would change data is refused. Your SQL must start with `SELECT`, `WITH`, `EXPLAIN`, `VALUES`, or `PRAGMA`. * **Query results are capped at 500 rows.** Larger results show a `≥ 500 rows` note and only the first 500; narrow the query, or use **Export table** for a larger bounded snapshot. * **A full-table export is one agent-side SQLite snapshot.** The current table, sort, and filter are captured when you click; the agent reads them in one read-only transaction, so concurrent WAL writes cannot mix database snapshots into the file. The CSV is not rebuilt from browser pages. * **Table CSVs stop at the first active cap:** 200,000 data rows or a 16 MiB response body (including the header and row separators). A byte-limited export keeps only complete CSV records. The completion message says whether the row or byte cap truncated the result. * **Grid and export truncation differ from the row drawer.** Grid cells are shortened at 2,000 characters and BLOBs are represented by their byte size; **Export current page** writes those displayed values. For the agent's full-table CSV, a TEXT value over 2,000 UTF-8 bytes is represented by its first 2,000 SQLite characters plus `…`, and a BLOB becomes ``. Open the row drawer for the separate full-cell read (itself bounded at 1,000,000 characters). * **CSV text is spreadsheet-safe.** Text beginning with `=`, `+`, `-`, `@`, tab, or carriage return is prefixed with an apostrophe before RFC 4180 quoting, preventing Excel or Sheets from treating stored database text as a formula. Numeric SQLite values remain numeric. * **No database yet?** Before you start the server for the first time, there's no database file. You'll see a *No database yet* message, start bunqueue once from **Control ▸ Server** to create it. * **Query history is per-browser.** Your last 10 successful queries are saved locally in this browser only; they aren't shared across devices. * **Counts can lag a few seconds.** The stats, table list, and rows refresh on their own timers, so on a busy server they may trail live writes slightly. * **Database and Workflow reads have a 5-second deadline; CSV exports have 15 seconds.** Each read runs in a disposable process in both source and compiled builds. A timeout or disconnected request kills that process and waits for it to exit. Two reads and one export can run concurrently. Up to 32 ordinary page reads may wait in a fair queue within the same five-second deadline; custom SQL refuses overload immediately. Full queues receive HTTP 429. Process-control endpoints remain available during slow scans. ::: details Under the hood (for developers) This screen talks to the local control agent (`:6800`, `/db/*` endpoints) via the `bq` client, never the bunqueue HTTP API. The agent uses a read-only SQLite connection plus a statement allowlist, so writes are impossible. * `GET /db/info`, store metadata (polls ~15 s) * `GET /db/tables`, table list + row counts (polls ~10 s) * `GET /db/tables//schema`, columns, indexes, DDL (polls ~30 s) * `GET /db/tables/`, a row page (polls ~6 s) * `GET /db/tables//export`, one filtered/sorted CSV from a single read transaction (on **Export table**) * `GET /db/tables//cell`, full value for a truncated cell (on drawer open) * `POST /db/query`, a custom read-only query (on Run) The export response is raw `text/csv; charset=utf-8`, not JSON. `Content-Length` and `X-Bunqueue-Db-Export-{Version,Table,Rows,Bytes,Cap}` let the client validate the table identity, exact byte count, and whether `rows` or `bytes` stopped the export before starting the download; the response is `no-store` and `nosniff`. Server-side limits: 500-row cap per query, 2,000-character grid-cell truncation, the full-table TEXT rule described above, 1,000,000-character full-cell cap, 200,000-row / 16 MiB full-table export caps, a 32 MiB child-response ceiling, and process deadlines of 5 seconds for reads and 15 seconds for exports. The child supervisor also exits if the agent disappears, including an abrupt kill. ::: --- --- url: /bunqueue-dashboard/docs/guide/mcp.md description: >- Connect bunqueue to AI agents (Claude Desktop, Claude Code) over the Model Context Protocol, with 73 tools, 5 resources, and 3 prompts, in embedded or TCP mode. --- # MCP Server bunqueue ships a Model Context Protocol server, `bunqueue-mcp`, that lets an AI agent (Claude Desktop, Claude Code) drive the queue with tools, resources, and prompts. ::: warning External MCP mutations bypass dashboard safety gates The upstream v2.9.3 MCP tool set includes Cancel, Discard, Drain, Obliterate, DLQ Retry and DLQ Purge. Those tools do not gain atomic generation/state/topology or reverse-dependency checks merely because an MCP client invokes them. The dashboard's own Copilot exposes only Promote, Pause and Resume mutations; its DLQ retry tool is absent. Grant the external MCP server write access only when you have independently established that flow topology cannot be stranded and that job-ID reuse cannot retarget the action. ::: It is a separate **stdio** process, launched by the MCP client rather than by this dashboard, and it is not part of the HTTP API on `:6790`. That is why the dashboard's **MCP** page (and this guide) is a setup and reference, not a live monitor. The server needs the optional peer dependency `@modelcontextprotocol/sdk`. ## Install the runtime Install Bunqueue and its optional SDK in a dedicated directory: ```bash mkdir bunqueue-mcp-runtime cd bunqueue-mcp-runtime bun add --exact bunqueue@2.9.4 @modelcontextprotocol/sdk@1.30.0 ``` Replace `/absolute/path/bunqueue-mcp-runtime` below with that directory's absolute path. `bun` must be on the MCP client's PATH. Installing both packages explicitly avoids depending on an optional peer being supplied by a transient `bunx` cache. The dashboard's local regression uses this public executable over stdio and the real authenticated TCP broker. ## Connection modes ### Embedded (default) Direct SQLite access, with no running server. Point `DATA_PATH` at the bunqueue database file. Best for a local agent on the same machine. ```json { "mcpServers": { "bunqueue": { "command": "bun", "args": ["/absolute/path/bunqueue-mcp-runtime/node_modules/.bin/bunqueue-mcp"], "env": { "DATA_PATH": "./data/bunq.db" } } } } ``` ### TCP (remote server) Connect to a running bunqueue server over its TCP protocol port (`6789`, distinct from the HTTP admin API on `6790`). Use `BUNQUEUE_TOKEN` if the server has one. ```json { "mcpServers": { "bunqueue": { "command": "bun", "args": ["/absolute/path/bunqueue-mcp-runtime/node_modules/.bin/bunqueue-mcp"], "env": { "BUNQUEUE_MODE": "tcp", "BUNQUEUE_HOST": "localhost", "BUNQUEUE_PORT": "6789", "BUNQUEUE_TOKEN": "your-token" } } } } ``` ## Where to put it For **Claude Desktop**, add the JSON above to `claude_desktop_config.json`. For **Claude Code**, register it from the CLI: ```bash claude mcp add bunqueue -- bun /absolute/path/bunqueue-mcp-runtime/node_modules/.bin/bunqueue-mcp ``` ## What it exposes ### Tools (73, in 12 categories) Every tool name is prefixed `bunqueue_`; the examples below drop the prefix. | Category | Count | Examples | | --- | --- | --- | | Jobs | 11 | `add_job`, `get_job`, `get_jobs`, `get_job_result`, `wait_for_job` | | Job management | 6 | `cancel_job`, `change_job_priority`, `promote_job`, `update_job_data` | | Consumption | 8 | `pull_job`, `pull_job_batch`, `ack_job`, `fail_job`, `job_heartbeat` | | Queues | 11 | `list_queues`, `pause_queue`, `resume_queue`, `drain_queue`, `obliterate_queue` | | Dead letter queue | 4 | `get_dlq`, `retry_dlq`, `purge_dlq` | | Cron | 4 | `add_cron`, `list_crons`, `get_cron`, `delete_cron` | | Flows | 4 | `add_flow`, `add_flow_chain`, `get_flow`, `get_children_values` | | Rate limits | 4 | `set_rate_limit`, `set_concurrency`, `clear_rate_limit` | | Webhooks | 4 | `add_webhook`, `list_webhooks`, `remove_webhook`, `set_webhook_enabled` | | Workers | 3 | `register_worker`, `unregister_worker`, `worker_heartbeat` | | Handlers | 3 | `register_handler`, `list_handlers`, `unregister_handler` | | Monitoring | 11 | `get_stats`, `get_queue_stats`, `get_memory_stats`, `get_prometheus_metrics` | ### Resources (5) `bunqueue://queues`, `bunqueue://stats`, `bunqueue://workers`, `bunqueue://crons`, `bunqueue://webhooks`. ### Prompts (3) `bunqueue_debug_queue`, `bunqueue_health_report`, `bunqueue_incident_response`. ## Verified TCP worker limitation With Bunqueue **2.9.4**, `register_worker` can return `success: true` with worker ID `"0"`, while the broker registry contains a different real ID. A heartbeat using `"0"` returns `success: false`. Read `list_workers`, match a unique worker name and its queues, and use that actual ID for heartbeat and unregister. Check the tool's JSON `success` field as well as MCP's `isError` flag. The regression verifies registration, the real registry ID, a successful heartbeat, display in the dashboard and removal. It also reads the stats resource and health-report prompt. This is not a test of every external MCP mutation. --- --- url: /bunqueue-dashboard/docs/guide/copilot.md description: >- An in-dashboard AI copilot for bunqueue. Bring your own model (Claude, ChatGPT, Gemini, GLM, or any OpenAI-compatible endpoint) and inspect or operate your queues by chat. --- # Copilot (experimental) The Copilot is a chat assistant built into the dashboard. It reads your live queue state and can propose only three mutation types, Promote, Pause and Resume, which you confirm before they run. It is experimental, and stays off until you add a model. ## Bring your own model Open the Copilot (the button in the bottom right), then the settings gear, and pick a provider: * Claude (Anthropic) * ChatGPT (OpenAI) * Gemini (Google) * GLM (Z.ai) * OpenRouter (one key, every model) * Custom (any OpenAI-compatible endpoint: Groq, Together, Mistral, a local Ollama or LM Studio) These are provider adapters supported by the dashboard through the Vercel AI SDK. A successful request also depends on the selected model, API credentials, endpoint compatibility and browser CORS policy. Paste your own API key and set the model id. The key is kept in memory for the session only and is never written to disk; the provider and model are remembered. Named providers (including Z.ai and OpenRouter) always use the endpoint built into the dashboard: a stale or injected saved base URL is discarded. Only **Custom** can use an operator-supplied base URL, and it must be a non-empty `http://` or `https://` endpoint without credentials, a query, or a fragment. ## Browser (CORS) note The dashboard is a browser app, so the model call goes straight from your browser to the provider. Some providers allow that and some do not: * Anthropic requests include its browser opt-in header. The dashboard also has direct adapters for Google, Z.ai and OpenRouter; their availability depends on the provider's current CORS and account policy. * OpenAI is marked as requiring a proxy in the provider selector. Use an endpoint that explicitly supports browser requests; a local custom endpoint must allow the dashboard origin too. * The local regression suite uses a scripted OpenAI-compatible HTTP endpoint to verify streaming, tool confirmation, decline and Stop against a real broker. It does not verify model inference or every external provider. See [Testing](/testing). ## What it can do Read (runs immediately): list queues and counts, list and inspect jobs, DLQ stats and entries, server health and stats, workers, and crons. Change (asks first): promote a delayed job, pause a queue or resume a queue. Those are the Copilot's only mutating tools. Cancel/remove, DLQ retry (single or bulk), completed-job requeue, generic retry and purge tools are intentionally absent. DLQ retry is not made safe by a preliminary read: in [Bunqueue v2.9.3](https://github.com/egeominotti/bunqueue/releases/tag/v2.9.3) the later POST has no atomic generation/state/topology precondition and can hit a different job recreated under the same ID. Completed-job requeue is also unavailable because `retryCompleted` does not reconstruct dependency registration or original flow order. Every mutating action shows a confirmation in the chat and only runs after you click Confirm. One chat turn is pinned to the server URL and Bearer token that were active when the turn started, so all of its reads stay on that same target. The confirmation label names that server. If the selected server or token has changed before approval, the write fails closed and no mutation request is sent. ## Try it In the [live demo](https://egeominotti.github.io/bunqueue-dashboard/) the Copilot answers against the demo fixtures, so with your own key you can watch it hold a real conversation and drive tools with no backend. --- --- url: /bunqueue-dashboard/docs/guide/usage.md description: >- A single, read-only snapshot of how hard your bunqueue server is working, lifetime job totals, live queue counts, error rate, memory, uptime, and disk… --- # Usage A single, read-only snapshot of how hard your bunqueue server is working, lifetime job totals, live queue counts, error rate, memory, uptime, and disk health. **Where:** open `/usage` from the sidebar. ![Usage](../screenshots/usage.png) ## What you'll see The page opens with a **Live** dot next to the title, that means everything on screen refreshes on its own. Below it sit six stat cards, then two detail cards: **Runtime** and **Storage**. The six top cards: | Card | What it tells you | | --- | --- | | **Completed** | Retained completed jobs reported by the dashboard snapshot. Always shown in green. | | **Failed** | Current failed jobs summed across queue summaries. Turns red when the number is above zero. | | **Waiting** | Jobs queued right now, waiting for a worker. Shown in amber. | | **Active** | Jobs being processed at this very moment. Shown in blue. | | **Error Rate** | Share of finished jobs that failed, as a percentage. Green when healthy, red once it passes 5%. | | **Uptime** | How long the server process has been running. Shows a dash (`—`) if the server just started. | The **Runtime** card breaks down the workload and process footprint: | Row | What it tells you | | --- | --- | | **Jobs pushed (since restart)** | Process-session push counter. | | **Jobs pulled (since restart)** | Process-session pull counter. | | **Heap used** | Memory actively in use by the server process. | | **RSS** | Total memory the process is holding. | | **Cron jobs** | How many scheduled (cron) jobs are registered. | The **Storage** card is a single health verdict: * **Healthy** (green), "Disk writes are being accepted." * **Disk full, writes suspended** (red), appears when the server can no longer write to disk. When available, it also shows the underlying error and how long ago writes stopped. ::: tip Large counts use your locale's thousands separator, so a value like `5825` may appear as `5,825` (or `5.825` in some locales). ::: ## What you can do This screen is a dashboard, not a control panel, there are no buttons that change anything on the server. * **Watch totals climb live.** Numbers update on their own as workers run; you never need to refresh. * **Spot trouble at a glance.** A red **Failed** or **Error Rate** card, or a red **Storage** panel, is your signal to jump to DLQ Control, Queue Control, or the server host to investigate. * **Retry when offline.** If the server can't be reached, a banner appears at the top with a **Retry** button that re-checks the connection. ## Good to know * **It's read-only by design.** Nothing here mutates the server, to act on failures, head to DLQ Control, Queue Control, or the server host. * **Uptime shows a dash, not `0m`,** when the server has just started or can't be reached. * **If the server goes offline,** an initial failure shows an error state. After a successful read, a refresh failure retains the last successful snapshot and labels it stale; missing storage data is not shown as a fresh healthy result. * **Counts have different lifetimes.** Completed/failed cards describe retained queue state; pushed/pulled are explicitly labelled since restart. Cleanup and retention can change state totals. The [classic appendix](/guide/classic) describes the older page. * **It's a focused summary.** Latency charts, throughput, and the full worker and cron lists aren't shown here, use Metrics and Workers for those. ::: details Under the hood (for developers) * Uses the `bq` client (not the legacy `api`). * Polls three endpoints in parallel each cycle: `GET /dashboard` (job stats, memory in MB, cron count) `GET /storage` (disk-health flag, wrapped in `data`), and `GET /queues/summary` (failed counts). * Refresh cadence follows the global interval from Settings (default **3000 ms**, floored at 500 ms). No SSE, pure polling, with change-detection to skip redundant re-renders. * `/dashboard` reports `uptime` in milliseconds and memory in megabytes; the page converts both before display. ::: --- --- url: /bunqueue-dashboard/docs/guide/s3.md description: >- Configure and operate Bunqueue 2.9.4 S3-compatible backups through the local control agent. --- # S3 Backup The **S3 Backup** screen configures and operates Bunqueue 2.9.4's official backup implementation. It supports AWS S3 and compatible providers such as R2, MinIO, DigitalOcean Spaces, and custom endpoints. **Where:** open `/s3` from the sidebar. ![S3 Backup](../screenshots/s3.png) ## Configure The form builds the exact environment accepted by Bunqueue: | Field | What it's for | | --- | --- | | Endpoint | Your provider's S3 URL (e.g. `https://s3.amazonaws.com`). | | Region | The bucket's region (defaults to `us-east-1`). | | Bucket name | The target bucket, this field also drives the green/amber badge. | | Access key ID | Your S3 access key. | | Secret access key | Your S3 secret, shown masked. | | Backup schedule | Disabled, every 6 hours, every 12 hours, or every 24 hours. | | Path prefix (optional) | A folder prefix inside the bucket, such as `backups/production/`. | | Backups to retain | Number of remote snapshots retained by scheduled cleanup. | | Addressing style | Path-style or virtual-hosted bucket addressing. | **Apply configuration** writes only the whitelisted `S3_*` keys into the local agent's managed server configuration. It preserves unrelated environment variables and requires confirmation. Restart the managed server before relying on the periodic scheduler. The status panel describes the configured values; its enablement badge alone does not prove a running process has reloaded them. The non-secret draft is stored in the browser. Access key, secret, and session token remain session-only and are never written to local storage. ## Operate The **Live backup operations** panel invokes the exact CLI shipped by the installed Bunqueue 2.9.4 dependency, with the managed `dataPath` and S3 environment: * **Status** reports scheduler enablement, bucket, endpoint, interval, and retention. * **Refresh** lists real remote backup objects with key, size, and timestamp. * **Backup now** creates a transactionally consistent snapshot after confirmation. * **Restore** is available only while the managed Bunqueue server is stopped. You must type `RESTORE`; the request also carries the exact database path/existence/size/WAL/SHM/mtime snapshot shown at confirmation time. If the database changes before the command runs, the agent rejects it. Only one backup command can run at a time. Child output is bounded, execution has a timeout, and the agent never invokes a shell. A remote dashboard target, unknown environment key, malformed object key, running-server restore, or stale database confirmation fails closed. ::: warning Local agent boundary These operations intentionally exist only through the dashboard control agent, which manages the database and process lifecycle. A static GitHub Pages demo or a dashboard pointed at an arbitrary remote Bunqueue server cannot perform local backup commands. Configure agent authentication before exposing it through a trusted proxy. ::: ::: details Under the hood (for developers) * Agent endpoints are `GET /backup/status`, `GET /backup/list`, and `POST` `/backup/configure`, `/backup/now`, `/backup/restore`. * All routes require a `target` that matches the server managed by the agent. * The official Bunqueue CLI JSON envelope is validated before it reaches React. * A read-only classic environment reference remains at `/s3-classic`; prefer `/s3` for operational use. ::: --- --- url: /bunqueue-dashboard/docs/guide/settings.md description: >- Configure named Bunqueue broker and control-agent profiles, credentials, theme, and polling. --- # Settings Settings owns the Dashboard's complete connection fleet. Each named profile pairs one Bunqueue HTTP API with the control agent responsible for that broker. **Where:** open `/settings` from the sidebar. ![Settings](../screenshots/settings.png) ## Connection profiles | Element | Purpose | | --- | --- | | **Active node** | Select the broker every ordinary Dashboard page currently drives. The same selector is always available under the sidebar. | | **Add node / Remove** | Create a profile (up to 32) or remove the selected one. The final profile cannot be removed. | | **Node name** | Human-readable fleet identity, for example `broker-eu-1`. | | **Server URL** | Bunqueue HTTP API, such as `/api` or `https://broker-1.example/api`. | | **Control agent URL** | The agent paired with this exact broker, such as `/agent` or `https://broker-1.example/agent`. | | **Bearer token** | Optional Bunqueue/bridge server credential for this profile only. | | **Agent token** | Independent `AGENT_TOKEN` for this profile's control agent. It is never sent to Bunqueue's HTTP API. | Click **Save** to validate and apply the complete draft atomically. Invalid, credential-bearing, protocol-relative, query-bearing, fragment-bearing, or non-HTTP(S) targets fail closed. Relative mount paths such as `/api` and `/agent` are supported. **Test connection** probes the Bunqueue `/health` endpoint currently typed in the form and checks its complete health/version shape. **Test agent** probes the typed `/control/status` target and checks its lifecycle response. Both use the unsaved draft credentials, have a ten-second deadline, and are cancelled when the draft, profile, or page changes. Switching the active profile changes the server URL, agent URL, and both token scopes in one state transition. Every poller, live stream, Flow/Workflow/Queue adapter, Database/S3 request, benchmark, alert read, and Copilot command follows the new identity. In-flight work is aborted or sequence-discarded, so rows from one node cannot remain actionable against another. ## Three brokers on PostgreSQL Create three profiles and pair each Bunqueue API with its own agent. Configure all brokers with the same `BUNQUEUE_POSTGRES_URL` and `BUNQUEUE_POSTGRES_NAMESPACE`, then open [Fleet](/guide/fleet). Fleet verifies the topology and lets you operate any node without first making it active. PostgreSQL shares queue state; it does not make process lifecycle, process logs, in-memory webhooks, or the agent's Workflow Engine SQLite store global. The Fleet guide lists every shared and node-local boundary. ## Appearance and refresh * **Theme** switches between Dark and Light immediately and persists. * **Refresh interval** controls ordinary polling at 1, 2, 3, 5, or 10 seconds. Feature-specific intervals still apply where documented (Fleet uses 10s, alerts use 15s, and the throughput sampler uses 1s). ## Persistence and security * Profile names, canonical server/agent URLs, active profile id, theme, and refresh interval persist in browser storage. * Server and agent tokens are stored only in module memory, isolated by profile, never serialized, and erased on reload or tab close. * Legacy v1-v3 connection blobs are sanitized into schema v4 and immediately rewritten without legacy token fields or unsafe authorities. * A blocked/full browser store does not lose the live edit: Settings reports that it was saved for this session only. * Fleet probing applies each inactive profile's own credentials directly and never temporarily retargets the active Dashboard. ::: tip Test before saving Both connection tests use the fields currently typed in the form. This lets you verify a new broker and its agent before applying the pair globally. ::: --- --- url: /bunqueue-dashboard/docs/guide/classic.md description: >- The first-generation classic view pages, kept intact per the additive rule, and how each maps to its Pro replacement. --- # Appendix, classic pages The first-generation pages, kept intact per the additive rule. Several have been superseded by a Pro page at the plain path (the classic version moved to a `-classic` suffix); `QueueDetail` is still the only per-queue drill-in view. ## Queue Detail (classic) > Route `/queues/:name` · source `src/pages/QueueDetail.tsx` ![Queue Detail (classic)](../screenshots/queue-detail.png) **What it shows:** The single-queue drill-in, opened by clicking a row on the classic Queues list. The screenshot shows the `emails` queue: six stat cards, a 12-row Recent Jobs table and a Configuration section. The route first verifies real membership through `/queues/summary`; an unknown queue exposes no mutations. Pause/Resume is guarded by server+queue ownership and a fresh membership check. Drain and Obliterate remain visible but disabled because v2.9.3 cannot prove cross-queue flow safety. Rate-limit and concurrency cards perform explicitly labelled blind desired-state writes with safe-integer validation, a mandatory rate window, explicit TTL mode, typed queue confirmation for clear, and timestamped receipts. **Differences vs the Pro page:** no Pro drill-in exists; [Queue Control](/guide/queue-control) offers the same actions plus stall/DLQ configuration, but via a queue dropdown rather than a per-queue URL. ## Overview (classic) > Route `/overview-classic` · source `src/pages/Overview.tsx` ![Overview (classic)](../screenshots/classic-overview.png) **What it shows.** A single-poll health summary of the whole bunqueue server: six stat cards (Waiting, Active, Completed, Failed, DLQ, Error Rate, DLQ and Error Rate turn red when non-zero / above 5%), a Throughput card with pushed/pulled/completed/failed rates per second, a Resources card (uptime, heap, RSS, and a Healthy/Disk-full storage flag), plus compact Workers and Cron Jobs lists (first 6 of each). In the screenshot the seeded server has 41,300 waiting jobs, two active workers (`emails`, `notifications`) and three crons (`nightly-sales-report`, `hourly-digest`, `cache-warmup`). Read-only, everything comes from one polled `api.overview()` call to the bunqueue HTTP API. **Differences vs the Pro page ([`/`](/guide/overview)):** OverviewPro adds a connection banner, a per-queue health grid, and a live Recent Activity feed. Uptime converts the server's millisecond counter before formatting, matching the Pro overview. ## Queues (classic) > Route `/queues-classic` · source `src/pages/Queues.tsx` ![Queues (classic)](../screenshots/classic-queues.png) **What it shows.** A live-polled, read-only list of every queue on the bunqueue server, 20 per page. Four stat cards summarize Waiting / Active / Delayed / DLQ counts; below them, each row shows the same counts per queue plus an Active/Paused badge, in the screenshot, six seeded queues (benchmark with 41,299 waiting, image-resize with 3 DLQ entries in red, reports, notifications with 3 delayed, maintenance, emails), all Active. The search box filters by name, and clicking any row drills into `/queues/:name` (QueueDetail). There are no actions here, pausing, draining, and limits live in Queue Control. **Differences vs the Pro page:** [`/queues`](/guide/queues) (QueuesOverview) fetches the full list in one `bq.queuesSummary()` call and adds inline pause/resume; this classic page is paginated and read-only. The search filters the current 20-row page; the header cards use global dashboard totals and therefore remain stable while paging. ## Jobs (classic) > Route `/jobs-classic` · source `src/pages/Jobs.tsx` ![Jobs (classic)](../screenshots/classic-jobs.png) **What it shows.** A cross-queue job explorer over the bunqueue HTTP API: six stat cards above a merged job table. Filter with the queue dropdown and the All/Waiting/Active/Completed/Failed segments or search by job ID. Cancel is deliberately unavailable because the server cannot inspect hidden reverse flow dependencies. "All Queues" is available for up to 100 discovered queues and displays the newest 100 jobs. Above that threshold the page makes no job-list requests and asks you to select a queue, avoiding a 10,000-request periodic fan-out. Each bounded load pins one server/token and is cancelled when the view or connection changes; arriving via `?queue=` preselects a queue. The optional Name value comes from `job.data.name`, Duration uses the server's `startedAt`/`completedAt` timestamps. Non-string `data.name` values are ignored instead of crashing the table. The queue picker refreshes every 30 seconds. **Differences vs the Pro page:** [`/jobs`](/guide/jobs) (JobsPro) is the replacement, single-queue, server-paginated, with multi-select bulk actions and correct Name/Duration. ## DLQ (classic) > Route `/dlq-classic` · source `src/pages/Dlq.tsx` ![DLQ (classic)](../screenshots/classic-dlq.png) **What it shows.** The first-generation dead-letter view: a queue selector (with per-queue DLQ counts), a "DLQ Entries" stat card, a paginated entries table (Job ID, Name, Reason, Attempts, Failed), and Retry all / Purge buttons that remain disabled by the same flow-safety policy. It polls the bunqueue HTTP API via the legacy `api` client. The client and table use the server's nested `{ job, enteredAt, reason, attempts[] }` shape, including the nested job id, attempt count and latest failure time. **Differences vs the Pro pages:** use [`/dlq`](/guide/dlq) (DlqPro, cross-queue filters plus CSV export) or [`/dlq-control`](/guide/dlq-control) (focused single-queue inspection). Both read the correct nested shape; every row/bulk/queue-wide retry control is disabled by the v2.9.3 atomicity policy. ## Cron (classic) > Route `/cron-classic` · source `src/pages/Cron.tsx` ![Cron (classic)](../screenshots/classic-cron.png) **What it shows.** A live-polling, read-only list of every scheduled job on the bunqueue server, paginated 15 per page. Each row shows the schedule's name, target queue, its trigger, a cron expression or an interval (`every 300000ms`), the next run time, and how many times it has executed. In the screenshot three schedules are registered: `nightly-sales-report` on the **reports** queue (`0 3 * * *`), `hourly-digest` on **emails** (`0 * * * *`), and `cache-warmup` on **maintenance** every 5 minutes (2 runs so far). The only action is the trash icon, which deletes a schedule after a confirmation prompt; if the server is unreachable an offline banner appears with a retry button. **Differences vs the Pro page.** This page is list + delete only, creating schedules lives in [Cron Manager](/guide/cron). No known bugs; the classic/Pro duplication is intentional. ## Metrics (classic) > Route `/metrics-classic` · source `src/pages/Metrics.tsx` ![Metrics (classic)](../screenshots/classic-metrics.png) **What it shows.** A read-only, auto-refreshing dump of the raw `GET /dashboard` payload from the bunqueue API. The top row is live throughput per second (Pushed/Pulled/Completed/Failed, 1.1 / 1.1 / 0.7 / 0.0 in the screenshot). Below sit lifetime Totals (5,582 pushed, 5,871 completed, 3 failed across the seeded emails/image-resize/reports/notifications/benchmark workload), server Memory (heap 78/96 MB, RSS 436 MB), latency percentiles per operation (`push`/`pull`/`ack` × p50/p95/p99), latency averages (`pushMs`/`pullMs`/`ackMs`), and the server's in-memory collections (`jobIndex` 41,881, `queuedTotal` 41,306, …). Nothing is clickable; it refreshes at the global polling interval from Settings. **Differences vs the Pro page:** [`/metrics`](/guide/metrics) (MetricsPro) adds a rolling 60-second throughput chart, a success-rate gauge, and per-queue counts, this page is flat key-value lists only. Note: the percentile list once rendered broken values (`[object Object]`/zeros); per [Known issues](/known-issues) it now correctly flattens the nested per-operation percentiles. ## Workers (classic) > Route `/workers-classic` · source `src/pages/Workers.tsx` ![Workers (classic)](../screenshots/classic-workers.png) **What it shows.** A live, read-only table of every worker registered with the bunqueue server, polled via the same `api.overview()` call the classic Overview uses. Two stat cards summarize Total and Active counts (2 / 2 in the screenshot). Each row lists the worker's name and full ID (here `notifications` and `emails` workers from the seeded demo), the queues it consumes, and its Active / Processed / Failed job counts plus a relative "Last Seen" timestamp (9s ago). The list is client-paginated at 20 rows; if the server truncates the list at 100 workers, an amber "showing first N of M" hint appears. Nothing here is clickable, the page is purely for monitoring throughput. **Differences vs the Pro page:** [`/workers`](/guide/workers) (WorkersPro) adds an active/stale status indicator and guarded registry cleanup for stale, idle records. The action does not stop worker processes. ## Logs (classic) > Route `/logs-classic` · source `src/pages/Logs.tsx` ![Logs (classic)](../screenshots/classic-logs.png) **What it shows.** A live activity feed of job events streamed over SSE from the bunqueue API, the same stream the Pro Logs page uses. Six stat cards count events since the page opened (Total, Completed, Failed, Waiting, Active) plus a rolling Throughput rate (2.8/s in the screenshot). Below, a table lists each event's status badge, event type, queue, relative timestamp, and job ID, 10 per page. Filter with the queue dropdown (list refreshed every 30 s), the All/Waiting/Active/Completed/Failed segments, or the search box (matches job ID or queue). In the screenshot, seeded `emails` and `notifications` jobs cycle through Waiting → Active → Completed. Counters reset on reload, this is a session view, not history. **Differences vs the Pro page:** [`/logs`](/guide/logs) adds richer failure details plus export/clear controls; both views use event type because SSE events do not carry job names. ## Usage (classic) > Route `/usage-classic` · source `src/pages/Usage.tsx` ![Usage (classic)](../screenshots/classic-usage.png) **What it shows.** A read-only snapshot of cumulative server usage, polled live from the bunqueue HTTP API via a single `api.overview()` call. Four stat cards give lifetime totals, in the screenshot the seeded demo workload shows 5,600 jobs pushed, 5,890 completed (green), 3 failed (red), 5,896 pulled (blue). Below them, a **Runtime** card lists uptime, heap used (79.0 MB), RSS (436.0 MB), workers ("2 active / 2") and cron jobs (3), and a **Storage** card shows honest disk-full state and when it began. There is nothing to click; if the server is unreachable the page renders zeroed values with an offline banner instead of an error. **Differences vs the Pro page:** [`/usage`](/guide/usage) (UsagePro) adds an error-rate figure and a more prominent storage warning. ## S3 Backup (classic) > Route `/s3-classic` · source `src/pages/S3Backup.tsx` ![S3 Backup (classic)](../screenshots/classic-s3.png) **What it shows.** A read-only reference for bunqueue's S3 snapshot backups. A banner reminds you that backups are configured on the **server via environment variables** and cannot be toggled from the dashboard. Two cards follow: **Storage status** (polls `GET /storage` on the bunqueue API and reports Disk plus any server Error) and **Configuration (server env)**, a static cheat-sheet of the eight variables that actually control backups (`S3_BACKUP_ENABLED`, `S3_BUCKET`, `S3_REGION`, `S3_ENDPOINT`, access/secret keys, `S3_BACKUP_INTERVAL`, `S3_BACKUP_RETENTION`) with defaults. There is nothing to click, use it as a lookup while editing your server's env. **Differences vs the Pro page:** [`/s3`](/guide/s3) (S3BackupPro) adds the environment builder and live agent-backed status, object listing, on-demand backup, and guarded restore operations. This classic page remains a read-only reference. ## Not found (404) > Route `*` · source `src/pages/NotFound.tsx` ![Not found](../screenshots/not-found.png) Any unknown path renders this catch-all inside the normal layout shell, the sidebar stays usable and a **Back to Overview** button returns to `/`. Note the Topbar falls back to a generic "bunqueue · bunqueue" title here, as it does for every route missing from its title map (see [Known issues](/known-issues)). --- --- url: /bunqueue-dashboard/docs/architecture.md description: >- How the bunqueue dashboard fits together: the React SPA, its polling and SSE data flow, the two API clients, and the local control agent. --- # Architecture ``` ┌────────────────────────────────────────────────────────────────────┐ │ Browser (SPA, :5273) │ │ │ │ main.tsx → (React Router) │ │ └─ AppLayout ─ Sidebar + Topbar + │ │ └─ pages/* and pages/control/* │ │ │ │ Data layer │ │ usePolledData(fetcher) ── interval ──► lib/bq.ts / lib/api.ts │ │ useActivityStream() ── SSE ───────► lib/sse.ts │ │ useThroughputSeries() ── 1s tick ───► bq.overview() │ │ stores/: theme · connection · alerts · s3 (Zustand + persist) │ └───────────┬──────────────────────────────┬────────────────────────┘ │ HTTP /api (proxy → :6790) │ /agent (→ :6800) ▼ ▼ ┌─────────────────┐ ┌───────────────────────┐ │ bunqueue server │ │ control agent (Bun) │ │ HTTP :6790 │◄──spawn────│ ProcessManager │ │ SSE /events │ /health │ public client + CLI │ └─────────────────┘ └───────────────────────┘ ``` The single-pair diagram is one connection profile. A production fleet repeats that server/agent pair per broker. The Dashboard stores up to 32 named profiles, and `/fleet` probes all pairs concurrently while the rest of the UI follows one atomic active identity. Brokers reporting the same credential-free PostgreSQL target and namespace are grouped as one shared queue topology; their control agents remain distinct node-local control planes. ## Components ### Feature-slice architecture New operational surfaces use a small hexagonal (ports-and-adapters) feature slice instead of importing transport code inside React components: ```text src/features// ├── domain/ # pure state, traversal, validation and selection rules ├── application/ # repository ports and use-case orchestration ├── infrastructure/ # Bunqueue HTTP/agent adapters and response validation └── ui/ # views and interaction state ``` Workflow, Job Flow, Queue SDK, S3, and Fleet operations follow this boundary. Tests inject repository ports into the UI and fake runtime ports into agent routes; real E2E scripts exercise the same adapters against a disposable Bunqueue 2.9.4 process. Non-idempotent commands use synchronous leases, while reads carry a target/request generation so a late response cannot cross a server, queue, workflow, or form retarget. The agent also owns one shared lifecycle gate across its local and bridged handlers. Workflow commands parse bounded input first, then atomically recheck the managed process state and generation before touching the Engine. Stop and restart close that Engine inside the same gate, so a slow request cannot revive runtime resources after the managed server has transitioned. * **Router & layout**, `App.tsx` declares every route (see [pages.md](pages.md) for the full, verified table, several routes' page-family assignment is not what the path name would suggest) under one `AppLayout` (`Sidebar` + `Topbar` + ``). See [components.md](components.md) for the layout shell in detail. * **Pages**, two families, distinguished by which API client they use, not by any visual marker: * `src/pages/*`, first-generation **classic** view pages. Use `lib/api.ts`. * `src/pages/control/*`, the **Pro**, full-control pages. Use `lib/bq.ts`. `pages/control/job/` and `pages/control/queue/` hold page-specific subcomponents too small to be their own page (e.g. `JobTimeline`, `JobBackoff`, `QueueActions`, `ConfigForms`). * The two families are not cleanly partitioned by route path. Pro pages render at the plain operational paths and the retained classic pages use `-classic` suffixes. Settings is shared, while Fleet is a direct multi-target operational page. See [pages.md](pages.md#route-table-from-src-app-tsx) for the authoritative table, don't infer family from the URL. * One page mixes clients: `LogsPro` calls `bq.queues()` for the queue filter dropdown but `useActivityStream` (shared with the classic `Logs` page) builds its SSE URL via `api.eventsUrl()`. Not a bug, the SSE endpoint is identical either way, but worth knowing if you're grepping for "does this page use `bq` or `api`". * **UI kit & stores**, see [components.md](components.md) for the full reference (`Card`, `StatCard`, `StatusBadge`, `Button`, `form.tsx`, `feedback.tsx`, `PageHeader`, `AreaChart`, `CopyButton`, inline SVG `icons`, and the four Zustand stores). ## Data flow ```mermaid flowchart LR subgraph Dashboard["Dashboard (React SPA)"] P["usePolledData
(interval poll)"] S["useActivityStream
(SSE reader)"] T["useThroughputSeries
(1s tick)"] W["Page actions
(pause / add / promote / …)"] end subgraph Server["bunqueue server :6790"] H["HTTP API"] E["/events SSE"] end A["Control agent :6800
(loopback, CORS-locked)"] Proc["bunqueue process"] P -->|"GET dashboard / summaries"| H T -->|"GET overview each second"| H S <-->|"stream job events"| E W -->|"POST / PUT / DELETE, then refetch"| H W -.->|"start / stop / restart"| A W -.->|"Flow / Workflow / backup"| A A -->|"public TCP client"| Proc A -->|"spawn / signal"| Proc Proc --- H Proc --- E ``` * **Polling.** `usePolledData(fetcher, deps)` runs immediately, then schedules the next tick only after the current request settles. Dependency/server/token generations hide the previous view synchronously, abort obsolete work, and discard late results. The last good snapshot remains visible on a same-scope refresh error, while identical serialized snapshots avoid a React re-render. Its connection generation includes active profile id, server URL, agent URL, and both credentials; switching nodes therefore hides old data before any action can target the new node. * **Live activity.** `useActivityStream(queue?)` streams SSE from `/events` (or `/events/queues/:q`) via a fetch-based reader (`lib/sse.ts`) that supports a bearer token, unlike `EventSource`. It keeps a bounded ring buffer of recent events (`MAX_EVENTS = 250`), cumulative counters, and a rolling 5s throughput. Powers `OverviewPro`'s Recent Activity and both `LogsPro`/`Logs`; any delivered frame proves liveness, and a clean stream end reconnects with bounded backoff. * **Throughput sampling.** `useThroughputSeries(windowSize=60)` is independent of both of the above, it ticks on its own 1-second `setInterval`, calling `bq.overview()` each time and appending `throughput.{pushPerSec,completePerSec,failPerSec}` into a rolling window for `MetricsPro`'s `AreaChart`. This means the chart's cadence is fixed at 1s regardless of `connectionStore.refreshMs`. * **Writes.** Page actions call `bq.*`/`api.*` and then `refetch()`, behind synchronous server/credential/owner leases and strict `{ok:true}` checks. Confirmations remain for authorized high-impact writes such as process lifecycle and webhook deletion. Cancel, queue Discard, Drain/Clean/Obliterate, every DLQ Retry/Purge and completed-job requeue are disabled because a confirmation cannot compensate for missing atomic generation/state/topology guarantees. DLQ `maxAge`/`maxEntries` are rendered read-only and omitted from saves; auto-retry can only be disabled. * **Job action gating.** Anywhere job lifecycle actions are rendered (`JobInspector`, `JobsPro`), the button set is computed by the single shared `lib/jobActions.ts::actionGates(state)`. Promote is available only for delayed jobs. DLQ retry remains false because a fresh exact-ID GET cannot atomically constrain the later POST, which may hit a recreated job; completed-job requeue remains false because `retryCompleted` does not rebuild dependency registration or flow order. Cancel and Discard are always false. See [api-mapping.md](api-mapping.md#job-action-gating) for the full table. * **Copilot mutations.** The assistant can read queue, job, DLQ, worker, cron and health data, but exposes only Promote, Pause and Resume as confirmed mutations. It has no DLQ retry or completed-job requeue tool. ## The API layer Two clients, on purpose (see the additive rule in the project `CLAUDE.md`): * **`lib/api.ts`**, the original client, used only by classic pages. It throws on non-2xx responses and logical HTTP-200 `{ ok:false }` failures, and its storage, job-timestamp and DLQ types mirror the server. * **`lib/bq.ts`**, the complete, shape-verified client behind every `pages/control/*` page and the control agent. Its `call()` helper throws on non-2xx **and** on a parsed `{ ok: false }` body, with one deliberate carve-out: `health()` passes `strict:false` because `GET /health`'s `ok` field means "server healthy" (can legitimately be `false` on disk-full with HTTP 200), not "request succeeded", see api-mapping.md for why that distinction matters and which other endpoints are strict. * Types for `bq` live in `lib/bqTypes.ts` (verified against a live server, see api-mapping.md); types for `api` live in `lib/types.ts`. * **New work always uses `bq`**, which exposes the complete control surface. ## The control agent A tiny local Bun process (`agent/`) that supervises a bunqueue server child process, because a browser can't start/stop an OS process and bunqueue's HTTP API has no process-lifecycle endpoint. See [agent.md](agent.md) for the full reference (endpoints, `ServerConfig`/`runningConfig` split, `dbStats()`). Because it can spawn processes it binds `127.0.0.1` only and is guarded by a **locked-CORS Origin allowlist** (never `*`) plus an optional `AGENT_TOKEN` bearer gate, see [agent.md](agent.md#security) and [Security policy](https://github.com/egeominotti/bunqueue-dashboard/blob/main/SECURITY.md). Keep its port on loopback (or an equally trusted network) regardless. The all-in-one server independently gates every remote/proxied administrative `/api/*` request with `BUNQUEUE_TOKEN`; this is not the agent credential. ## Theming Tailwind CSS v4 with CSS-variable tokens (`--bg`, `--surface`, `--line`, `--fg`, `--muted`, `--accent`, …) mapped into Tailwind via `@theme inline`, so utilities like `bg-surface` / `text-muted` / `border-line` flip instantly when `data-theme` changes. Dark is the default; `light:` is a custom variant. Inter + JetBrains Mono (variable) via Fontsource; numbers use tabular figures (`.tnum`). `themeStore.initTheme()` applies the persisted theme before the first render (no flash-of-wrong-theme); see the Stores section in [components.md](components.md). --- --- url: /bunqueue-dashboard/docs/pages.md description: >- The verified route-to-page-to-API-client table for every page in the dashboard, both classic and Pro. --- # Pages Every route registered in `src/App.tsx`, what it renders, which API client it uses, and what it actually does. Verified against the current source; where a route's label in the sidebar doesn't match the page family you'd expect, that's called out explicitly below. ## How to read this page Two page families coexist by design (see [architecture.md](architecture.md)): * **Pro** (`src/pages/control/*`), the corrected, complete-control surface. Uses `lib/bq.ts`. This is where new work happens. * **Classic** (`src/pages/*`, first-gen), the original read-mostly view pages. Uses `lib/api.ts`. Kept intact per the additive rule; not actively extended. Every nav-reachable single-node operation uses the Pro client except the shared `/settings` page and browser-local `/alerts`; `/fleet` intentionally uses captured direct targets so it can probe inactive profiles. Each classic page remains routable at a `-classic` suffix. ## Route table (from `src/App.tsx`) | Path | Component | Family | Client | | --- | --- | --- | --- | | `/` | `control/OverviewPro` | Pro | `bq` | | `/fleet` | `Fleet` | Multi-node operations | direct target-pinned server + agent probes | | `/overview-classic` | `Overview` | Classic | `api` | | `/queues` | `control/QueuesOverview` | **Pro** | `bq` | | `/queues/:name` | `control/QueueDetailPro` | **Pro** | `bq` | | `/queues-classic` | `Queues` | Classic | `api` | | `/queues-classic/:name` | `QueueDetail` | Classic | `api` | | `/jobs` | `control/JobsPro` | **Pro** | `bq` | | `/jobs-classic` | `Jobs` | Classic | `api` | | `/dlq` | `control/DlqPro` | **Pro** | `bq` | | `/dlq-classic` | `Dlq` | Classic | `api` | | `/cron` | `control/CronManager` | **Pro** | `bq` | | `/cron-manager` | redirect → `/cron` | n/a (legacy alias) | n/a | | `/cron-classic` | `Cron` | Classic | `api` | | `/workflows` | `control/Workflows` | Pro | `bq` + agent Engine/SQLite adapter | | `/workflows/executions` | `control/Workflows` | Pro | active execution explorer | | `/workflows/waiting` | `control/Workflows` | Pro | durable waiting/signal control | | `/workflows/compensation` | `control/Workflows` | Pro | saga compensation control | | `/workflows/archive` | `control/Workflows` | Pro | archive audit + maintenance | | `/flows` | `control/Flows` | Pro | `bq` + agent FlowProducer adapter | | `/metrics` | `control/MetricsPro` | **Pro** | `bq` | | `/metrics-classic` | `Metrics` | Classic | `api` | | `/workers` | `control/WorkersPro` | **Pro** | `bq` | | `/workers-classic` | `Workers` | Classic | `api` | | `/logs` | `control/LogsPro` | **Pro** | `bq` + SSE | | `/logs-classic` | `Logs` | Classic | `api` | | `/server` | `control/ServerControl` | Pro | `bq` (+ control agent) | | `/add-job` | `control/AddJob` | Pro | `bq` | | `/jobs/bulk-add` | `control/BulkAddJobs` | Pro | `bq` | | `/job` | `control/JobInspector` | Pro | `bq` | | `/queue-control` | `control/QueueControl` | Pro | `bq` | | `/dlq-control` | `control/DlqControl` | Pro | `bq` | | `/webhooks` | `control/Webhooks` | Pro | `bq` | | `/diagnostics` | `control/Diagnostics` | Pro | `bq` | | `/alerts` | `Alerts` | Client-side rules (see below) | `alertsStore` + `useAlertEngine` | | `/benchmark` | `control/Benchmark` | Pro | `bq` | | `/database` | `control/Database` | Pro, read-only SQLite inspector via the control agent's `/db/*` endpoints | `bq.db` (agent) | | `/mcp` | `control/McpServer` | Pro (static setup guide) | none | | `/usage` | `control/UsagePro` | **Pro** | `bq` | | `/usage-classic` | `Usage` | Classic | `api` | | `/s3` | `control/S3BackupPro` | **Pro** | `bq` + `s3Store` | | `/s3-classic` | `S3Backup` | Classic | `api` | | `/settings` | `Settings` | Classic (the only settings page) | `api`, `connectionStore`, `themeStore` | | `*` | `NotFound` | n/a | n/a | `/cron-manager` was a duplicate route serving the same `CronManager` page as `/cron`; it is now a `` redirect to `/cron` so old bookmarks keep working. ## Sidebar → page mapping {#sidebar-page-mapping} `src/components/layout/Sidebar.tsx` (`NAV`, also consumed by the Cmd/Ctrl-K command palette) groups nav items into five sections plus root Overview/Fleet: * **Root**: Overview · Fleet. * **Queues**: Queues (QueuesOverview) · Jobs (JobsPro) · Dead Letter Queue (DlqPro) · Cron Jobs (CronManager). * **Workflow**: Overview · Job Flows · Executions · Waiting & Signals · Compensation · Archive. * **Monitoring**: Metrics (MetricsPro) · Workers (WorkersPro) · Logs (LogsPro) · Alerts. * **Control**: Server · Add Job · Bulk Add · Job Inspector · Queue Control · DLQ Control · Webhooks · Diagnostics · Benchmark, all Pro, all `bq`. * **Management**: Database · MCP · Usage (UsagePro) · S3 Backup (S3BackupPro) · Settings (classic). There are still **three DLQ pages** (`DlqPro` at `/dlq`, `DlqControl` at `/dlq-control`, classic `Dlq` at `/dlq-classic`), intentional per the additive rule, not an oversight: `/dlq` is the cross-queue dashboard, `/dlq-control` the single-queue triage surface. ## Home & Control (Pro, `bq`) | Route | Page | Behaviour | | --- | --- | --- | | `/` | `OverviewPro` | Connection banner (host · uptime · RAM) that flips to an amber "Connection lost, showing last known data / Stale" state when a poll fails after the first success; two rows of stat cards, a Queue Health grid, and a live Recent Activity feed from `useActivityStream()`. | | `/fleet` | `Fleet` | Polls every configured Bunqueue API and paired agent independently, groups matching PostgreSQL target/namespace members, drives per-node Start/Stop/Restart, and atomically selects the node used by every other page. | | `/server` | `ServerControl` | Start/Stop/Restart via the control agent (amber "agent unreachable" banner + disabled lifecycle buttons when the agent poll dies); storage row (SQLite main/WAL/total-on-disk/last-modified); always-editable config form with port validation, a "Save & restart" shortcut and a "restart to apply" hint; colour-coded live process-log tail (`stdout`/`stderr`/`sys`). | | `/add-job` | `AddJob` | Enqueue with every option the v2.9.3 HTTP single-push route forwards, including tags/group/dependencies, structured backoff and interval repeat policy; single or repeated copies via a `Count` field (validated, ≤10000). | | `/jobs/bulk-add` | `BulkAddJobs` | Bulk enqueue: paste a JSON array or NDJSON (one JSON value per line). Spec mode preserves the validated operator-safe v2.9.3 `JobInput` subset and maps `jobId`/`customId` correctly; raw mode keeps the whole item as data. Flow topology/failure fields belong to the atomic Flow API, while inert compatibility-only fields are rejected instead of implying support. | | `/job` | `JobInspector` | Look up a job by ID (deep-linkable via `?id=`). Kv overview, editable Data, separately fetched Result, Error + stacktrace, Timeline, Backoff preview, logs and children. Promote and eligible metadata/progress writes are state-gated; DLQ retry and completed-job requeue are unavailable. 404 ("Job not found") is distinguished from network/5xx errors. | | `/queue-control` | `QueueControl` | Per-queue counts; guarded pause/resume and promote-delayed, with retry-completed and flow-unsafe Drain/Clean visible but disabled; blind desired-state rate/concurrency controls with explicit window/TTL, typed clear and receipts; Stall/DLQ forms where auto-retry can only be disabled and `maxAge`/`maxEntries` are read-only and omitted from saves. | | `/cron` | `CronManager` | Create a schedule (cron expression *or* interval-in-ms, live next-runs preview) with v2.9.3 dedup and complete spawned-job options, then list/delete schedules. | | `/dlq-control` | `DlqControl` | Single-queue DLQ triage. Removal, row retry, Retry-all and Purge are unavailable under the v2.9.3 atomicity policy. | | `/dlq` | `DlqPro` | Cross-queue DLQ dashboard with filters, pagination and CSV export. Individual removal, row/global/queue retry and Purge are unavailable. | | `/webhooks` | `Webhooks` | Create (URL, optional queue scope, optional HMAC secret, event checkboxes from `WEBHOOK_EVENTS`), list with success/failure counts and last-triggered, enable/disable toggle, delete (confirmed). | | `/diagnostics` | `Diagnostics` | Health/version/uptime/disk cards, a manual Ping button (round-trip ms), WS/SSE client counts, storage error, memory (heap/RSS), lifetime totals. | | `/benchmark` | `Benchmark` | Interactive load benchmark on a random per-tab queue, with empty-queue safety preflight, exact run-id accounting, foreign-job restoration, presets, live throughput chart and run history. | | `/flows` | `Flows` | Three-part Flow console: HTTP DAG explorer; all five official `FlowProducer` creation methods; safe Flow Job inspections, dependency/result reads, bounded wait, and durable mutations through the managed TCP adapter. | | `/workflows` | `Workflows` | Temporal-style command centre over Bunqueue's persisted Workflow Engine state, with live Engine start/recover/reload controls. Dedicated routes provide execution exploration, durable signals, saga compensation, and terminal archive/cleanup. | | `/database` | `Database` | Read-only SQLite inspector over the agent: tables, schema/indexes/DDL, sortable + filterable data grid, row detail drawer, and a query runner (SELECT-only allowlist, 500-row cap, history, EXPLAIN, CSV/JSON export). | | `/mcp` | `McpServer` | Static setup/reference guide for the `bunqueue-mcp` stdio MCP server: config snippets with copy buttons, not a live monitor. | ## Queues / Jobs / Metrics / Logs / Usage / S3 (Pro, `bq`) | Route | Page | Behaviour | | --- | --- | --- | | `/queues` | `QueuesOverview` | Queue list over `bq.queuesSummary()` (search, totals, client-side pagination); click a row to drill into `QueueDetailPro`. | | `/queues/:name` | `QueueDetailPro` | Verified single-queue drill-in on the same building blocks as `QueueControl`, with Obliterate visibly disabled, a live backlog-depth sparkline, recent jobs, and links to Jobs/DLQ. | | `/jobs` | `JobsPro` | Single-queue, server-paginated job explorer: queue + status filters, stat cards and checkbox multi-select. Promote is the only enabled row/bulk job-lifecycle mutation; DLQ retry and completed-job requeue fail closed via the shared `actionGates`. | | `/metrics` | `MetricsPro` | Live throughput area chart (rolling 60s via `useThroughputSeries`), error/success-rate gauge, server-overview Kv list, per-queue counts table. Latency strip reads the real per-operation percentiles (`push`/`pull`/`ack` × p50/p95/p99). | | `/logs` | `LogsPro` | Paginated, filterable (queue/status/search) view over the same live SSE stream `useActivityStream` drives on `OverviewPro`, a fuller UI over the identical 250-event ring buffer, not a separate data source. | | `/workers` | `WorkersPro` | Registered-workers table over `bq.workers()`, with active/stale status and confirmed registry cleanup only for stale workers reporting zero active jobs. The cleanup does not stop the process. Caps at 100 rows with a truncation hint. | | `/usage` | `UsagePro` | Cumulative totals, error rate, runtime, and an honest Storage health card from `bq.storage()` (red "Disk full, writes suspended" when `diskFull`). Renders uptime correctly (`stats.uptime` is ms). | | `/s3` | `S3BackupPro` | S3-compatible environment builder plus live Bunqueue 2.9.4 CLI status/list/backup operations. The agent applies only whitelisted `S3_*` keys and permits restore only while stopped with typed confirmation and an unchanged database snapshot. | ## Alerts (client-side) `/alerts` (`src/pages/Alerts.tsx` + `alertsStore`) manages threshold rules and channels; the rules are evaluated **in the browser** by `useAlertEngine` (mounted app-wide via `AlertEngine`): 15s poll, edge-triggered breach detection, per-rule cooldown, in-app toast + optional desktop Notification. Delivery channels (email/webhook/slack) have **no backend** in bunqueue OSS, so see [known-issues.md](known-issues.md) for the real limits. ## First-gen view pages (Classic, `api`) These predate the Pro pages. All are off-nav (`-classic` routes) and superseded by a Pro page, but their shared API shapes and basic readouts remain correct and regression-safe. | Route | Page | Behaviour | | --- | --- | --- | | `/overview-classic` | `Overview` | Stat cards + throughput + resources + a compact workers/crons summary. | | `/queues-classic` | `Queues` | Paginated queue list with client-side page search and global header totals. | | `/queues-classic/:name` | `QueueDetail` | Membership-verified drill-in: guarded pause/resume, disabled Drain/Obliterate, counts, recent jobs and blind desired-state policy cards. | | `/jobs-classic` | `Jobs` | Bounded cross-queue explorer with safe display names and no destructive Cancel action. | | `/dlq-classic` | `Dlq` | Single-queue DLQ table using the real nested `{ job, enteredAt, reason, attempts[] }` shape. | | `/cron-classic` | `Cron` | List + delete only (no create form, use `/cron`). | | `/metrics-classic` | `Metrics` | Kv dumps of the raw `/dashboard` payload (latency/collections/totals/memory), useful for inspecting the raw shape. | | `/workers-classic` | `Workers` | Registered-workers table, no status/unregister. | | `/logs-classic` | `Logs` | Same SSE feed as `LogsPro`, with event type, queue, timestamp and job id. | | `/usage-classic` | `Usage` | Cumulative totals + runtime + honest storage health and disk-full timestamp. | | `/s3-classic` | `S3Backup` | Read-only storage status + a static list of the server env vars that configure S3 backup. | ## Settings `/settings` (`Settings`) is shared by both families. It creates, edits, tests, selects, and removes named broker profiles (`baseUrl` + paired `agentBaseUrl`), keeps a separate in-memory server/agent token pair per profile, and controls theme plus the global poll-refresh interval. Switching profiles retargets all clients, streams, pollers, and agent adapters as one connection generation. ## Layout shell (every route) `App.tsx` wraps every route above in one `AppLayout` (`Sidebar` + `Topbar` + ``), with two error boundaries (app-wide + per-page, reset on any navigation) and a single `` around the outlet so the shell never blanks during a lazy-chunk load. See [architecture.md](architecture.md) for the shell and [components.md](components.md) for `Sidebar`/`Topbar`/`SidebarFooter` details. --- --- url: /bunqueue-dashboard/docs/components.md description: >- The dashboard's UI kit, Zustand stores, and shared lib: what each component and hook does, and how to reuse it. --- # Components, stores & shared lib Reference for everything under `src/components/` and the non-API modules under `src/lib/`, the pieces every page is built from. See [architecture.md](architecture.md) for how these fit together and [pages.md](pages.md) for what each page does with them. ## Layout shell (`src/components/layout/`) * **`AppLayout.tsx`**, the only layout in the app. `flex h-screen` row: `Sidebar` + a column of `Topbar` and a scrollable `
` holding ``. Every route in `App.tsx` renders inside this one shell. * **`Sidebar.tsx`**, the `NAV` constant (a `NavGroup[]`) is the single source of truth for what's navigable and for the command palette. It renders root Overview/Fleet links plus Queues, Workflow, Monitoring, Control, and Management sections. Below the nav, `ConnectionBadge` is a live profile selector and health indicator; changing it atomically retargets the whole app. `ThemeToggle` (flips `themeStore`), and `SidebarFooter`. * **`SidebarFooter.tsx`**, identity card at the very bottom (bq logo badge + current host from `connectionStore` + a settings shortcut). * **`Topbar.tsx`**, a sticky header showing a breadcrumb-style title (`TITLES` record keyed by pathname, falls back to `"bunqueue"`) and a settings-link avatar. See [known-issues.md](known-issues.md) for the routes its title map doesn't cover. ## UI kit (`src/components/ui/`) Small, dependency-free primitives. No component library, everything here is hand-rolled Tailwind + inline SVG. | File | Exports | Notes | | --- | --- | --- | | `Card.tsx` | `Card`, `CardHeader`, `SectionTitle` | `Card` is a bordered/padded box (`padded={false}` to manage your own padding, e.g. for tables). `CardHeader` puts a title + optional icon + optional right-aligned action on one row. | | `StatCard.tsx` | `StatCard`, `StatTone` | Label/value/optional-hint block; `tone` maps to a text colour (`default`/`green`/`red`/`blue`/`amber`/`accent`); `compact` shrinks padding/font for dense grids. | | `StatusBadge.tsx` | `StatusBadge`, `StatusDot` | `StatusBadge` is the pill used everywhere a job/queue state is shown, its `STYLES` map (`waiting`/`active`/`completed`/`failed`/`delayed`/`prioritized`/`paused`/`waiting-children`/`stalled`) is the canonical colour-per-state mapping; reuse it (``) rather than re-deriving colours. `StatusDot` is the smaller "Live"/"Paused"/"Healthy" indicator used in page headers. | | `Button.tsx` | `Button`, `IconButton` | `Button` variants: `default`/`ghost`/`accent`/`danger`/`warning`/`success`; sizes `sm`/`md`. `IconButton` is a square 32px button for row actions (defaults to `ghost`). Neither manages a loading spinner, callers pass `disabled={busy}` themselves. | | `CopyButton.tsx` | `CopyButton` | Copies `value` via `navigator.clipboard.writeText`, flashes a check icon for 1.5s. Fails silently if the Clipboard API is unavailable (insecure context / permissions), no error surfaced, by design (a copy button isn't worth an error banner). Added for `JobInspector`'s copyable job/custom IDs; reusable anywhere an ID needs copying. | | `form.tsx` | `Label`, `Field`, `Input`, `Select`, `Toggle`, `SegmentedControl` | `Field` = `Label` + child, the standard label/control pairing used in every form on the site. `Input`/`Select` share one Tailwind class string (`controlClass`) so they always look identical. `Toggle` is a bespoke switch (not a native checkbox, `role="switch"`). `SegmentedControl` is the pill-group filter (e.g. status tabs on `JobsPro`/`LogsPro`), generic over the option type so callers get type-checked `value`/`onChange`. | | `feedback.tsx` | `Spinner`, `LoadingState`, `EmptyState`, `ErrorState` | The three states every polled page cycles through: `LoadingState` (first load, no data yet), `ErrorState` (first load failed, shows `error.message` + an optional retry button), `EmptyState` (loaded successfully, nothing to show, icon + title + hint + optional action). Convention: check `loading && !data` before `error && !data` before rendering content, so a slow-but-eventually-successful load doesn't flash an error. | | `Pagination.tsx` | `Pagination` | The shared pager under **every** list table. Two modes: pass `total` (known count, from server endpoints that return it, or a fully-loaded client list) to get "X to Y of Z" and a disabled Next on the last page; pass `hasNext` instead (unknown total, `GET /queues/:q/jobs/list` returns none) to drive Next off "did this page come back full". `page` is 0-based; it self-hides when a single page of known data fits. Callers keep a `const [page,setPage]=useState(0)` and either refetch with `offset=page*PAGE_SIZE` (server pagination) or `slice` the in-memory list (client pagination). Reset page to 0 when a filter/queue changes. | | `PageHeader.tsx` | `PageHeader` | Every page's top block: `title` + optional `live` dot (green `StatusDot`) + `description` + right-aligned `actions` + optional `back` control. | | `AreaChart.tsx` | `AreaChart`, `ChartSeries` | Pure-SVG multi-series line/area chart, no charting library. Renders a **rolling window**, `x` maps linearly over `points.length`, it does not know about real timestamps, so all series passed to one chart must share the same sampling cadence (see `useThroughputSeries`). Only consumer today is `MetricsPro`'s throughput chart. See [known-issues.md](known-issues.md) for its `NaN`/`Infinity` sensitivity. | | `icons.tsx` | `Icon*` (24 icons) | Inline SVG, one export per icon, all built on a shared `` wrapper (`stroke="currentColor"`, `1.75` width) so they inherit text colour and size uniformly via `className`. No icon package dependency. | ## Stores (`src/components/dashboard/stores/`, Zustand + `persist`) All four stores use `zustand/middleware`'s `persist` to `localStorage`, so settings survive a refresh. `test/setup.ts` shims `localStorage` so these import cleanly under `bun test`. * **`connectionStore.ts`**, named profiles containing Bunqueue API and paired agent URLs, an active profile id, two memory-only credentials per profile, and `refreshMs`. Up to 32 canonical, credential-free profile records persist in schema v4; v1-v3 blobs migrate without restoring tokens. Server/agent clients and SSE re-read the active identity on every request. Captured target clients freeze one profile and its exact credentials for multi-step commands, while `usePolledData` treats any profile/URL/token change as a new generation and aborts obsolete work. * **`themeStore.ts`**, `{ theme: 'dark'|'light' }`. `applyTheme()` sets `document.documentElement.dataset.theme` (which Tailwind's `light:` variant keys off, see architecture.md) and the native `color-scheme` CSS property. `initTheme()` is called once from `main.tsx` **before** the first render so there's no flash-of-wrong-theme; `onRehydrateStorage` re-applies it after the persisted value loads. * **`alertsStore.ts`**, `{ channels, rules }`, local-only; the routed Alerts page evaluates enabled rules against the active profile. * **`s3Store.ts`**, S3 connection-settings draft for `S3BackupPro`, local only, never sent to the server (see [pages.md](pages.md)). ## Shared `lib/` (non-API) * **`format.ts`**, every number/time/byte formatter used across the app (`formatNumber`, `formatPercent`, `formatDateTime`, `formatRelativeTime`, `formatDuration`, `formatUptime`, `formatBytes`, `errorRate`, `jobDuration`). Pure functions, fully unit-tested in `test/format.test.ts`. Always reach for one of these instead of formatting inline, consistency (`.` thousands separators, `it-IT`-style datetimes) depends on it. * **`cn.ts`**, a 4-line className joiner (`cn(...parts)` → filters falsy, joins with spaces). Not `clsx`/`tailwind-merge`; no dedup or conflict resolution, just concatenation. Fine because Tailwind classes here are written statically, not dynamically composed in ways that would conflict. * **`jobActions.ts`**, `actionGates(state)`, the single source of truth for "which job actions may the dashboard safely expose". This is deliberately narrower than what the upstream server would accept: DLQ retry and completed-job requeue always fail closed. Shared by `JobInspector` (single-job actions) and `JobsPro` (per-row + bulk actions) so the two pages can never drift on what's legal, see [api-mapping.md](api-mapping.md#job-action-gating) for the full state table. * **`usePolledData.ts`** / **`useActivityStream.ts`** / **`useThroughputSeries.ts`** the three data-fetching hooks; see [architecture.md](architecture.md#data-flow). Notes worth knowing when writing a page: * `usePolledData(fetcher, deps, { intervalMs })`, the optional third arg overrides the global refresh for that hook. Use a large value (e.g. `{ intervalMs: 30000 }`) for a rarely-changing queue-name **dropdown** so it doesn't re-poll on the fast activity cadence. The hook is **self-scheduling** (one fetch in flight at a time), **pauses while the tab is hidden**, and **sequence-guards** stale resolutions, so it's safe to include `page` in `deps` and refetch on page change without racing. * `useThroughputSeries()` returns `{ push, complete, fail, depth }` rolling windows (one 1s poller feeds all four) plus a `depthTrend(depth)` helper that returns the backlog slope (`draining` / `accumulating` / `steady`), used by MetricsPro's Queue Depth chart. * **`sse.ts`**, the fetch-based SSE frame reader; see architecture.md. --- --- url: /bunqueue-dashboard/docs/agent.md description: >- The local Bunqueue operator agent: process lifecycle, Flow and Workflow controls, backups, security model, and read-only SQLite inspection. --- # Control agent ## Why it exists A browser cannot start or stop an OS process, and bunqueue's HTTP API has no process-lifecycle endpoint (and we don't modify bunqueue). So the dashboard ships a tiny **local agent**, a Bun process that supervises a bunqueue server child. It also hosts target-pinned Flow, Workflow, and Queue operations through Bunqueue's public 2.9.4 client, runs the pinned backup implementation, and exposes read-only SQLite observability that the browser cannot perform directly. ## Files * `agent/manager.ts`, `ProcessManager`: `start()`, `stop()`, `restart()`, `getStatus()`, `getLogs()`, `getConfig()`, `setConfig()`, `dbStats()`. Spawns the configured command with `Bun.spawn`, pipes stdout/stderr into a bounded log ring buffer, and on `stop()` sends SIGTERM then SIGKILL after an 8s timeout. Tracks `runningConfig`, the config the live process was launched with, separately from the editable `config`, so editing ports/data-path while running does not confuse the health probe. Every process generation carries a monotonic **process token**; `onExit`/`stop()` only mutate shared state when their token is still current, so a `stop()` awaiting an old process can't clobber one a concurrent `start()` brought up. `dbStats()` stats the configured SQLite file plus its `-wal`/`-shm` sidecars. * `agent/server.ts` and `agent/server/`, request routing, bounded JSON parsing, error mapping, **auth/Origin policy**, and the shared process/Workflow lifecycle gate, factored so each boundary is unit-testable without binding a port. * `agent/flow/`, official `FlowProducer` create/read/result and safe Flow Job operations; `agent/workflow/`, persistent `Engine` lifecycle plus read-only execution storage; `agent/queue/`, the missing live Queue SDK contracts. * `agent/backup/`, one serialized, cancellable backup runner. Source mode launches the installed CLI without a shell; compiled mode invokes the same pinned command inside an embedded worker, avoiding executable recursion. * `agent/index.ts`, thin `Bun.serve` wrapper. **Binds `127.0.0.1` only** and applies the security policy below. ## Saved server configuration The source agent and standalone dashboard load `.bunqueue-dashboard/config.json` relative to their launch directory. Set `AGENT_CONFIG_PATH` to a stable absolute path for services, and use a separate path for each agent. Environment variables supply initial defaults only when no saved configuration exists. The saved snapshot contains the complete command, ports, database path and `extraEnv` values. `PUT /control/config` validates the change, writes and syncs a private temporary file, then atomically replaces the snapshot before updating memory or returning success. A failed save keeps the previous configuration and revision. A corrupt or unsupported saved file stops startup with an explicit error. Remove the file to reset to environment defaults. Every restart receives a fresh revision seed, so browser edits captured before that restart cannot overwrite the new state. The agent never auto-starts the managed server after loading settings. Saved changes to a running server take effect on its next start; `runningConfig` keeps describing the current child. The file includes environment secrets: POSIX files are created with mode `0600` in a `0700` directory; Windows access follows the containing directory's ACL. Keep this directory private and outside version control. ## Security The agent can spawn arbitrary processes (`PUT /control/config` sets the launch command; `POST /control/start` runs it), so binding loopback is not enough, a malicious web page the user is visiting could otherwise issue a cross-origin request to `http://127.0.0.1:6800` (CSRF → RCE). Defenses: 1. **Locked CORS**, the `Access-Control-Allow-Origin` header is reflected only for an allowed origin, **never `*`**. A disallowed origin gets no ACAO, so the browser blocks it. 2. **Origin allowlist**, any request carrying a disallowed `Origin` header is rejected `403` before it reaches the `ProcessManager`. A cross-origin browser request always sends `Origin`, so a drive-by page cannot start/stop/reconfigure the server. Non-browser callers (curl, same process) send no `Origin` and keep working for local use. 3. **Host allowlist (DNS-rebinding defense)**, the Origin gate can't see a *same-origin* request (a page whose DNS was rebound to `127.0.0.1` sends no `Origin`), so the agent also rejects `403` any request whose `Host` header is a hostname outside the allowlist, so an attacker domain rebound to loopback fails it, while `Host: localhost` / `127.0.0.1` pass. A missing `Host` (a non-browser caller) is rejected once the allowlist is enabled. Host and Origin are validated before an `OPTIONS` preflight receives `204`. 4. **Scoped bearer token**, on loopback `AGENT_TOKEN` protects state-changing requests (`Authorization: Bearer ` or `x-agent-token: `) while local reads remain zero-configuration. A LAN or reverse-proxied all-in-one bridge requires the token and applies it to **every** agent route, including status, logs, config, and database reads. Enter the token under **Settings → Agent token** or in the authentication prompt; it remains in browser memory for the current session. Never put it in a `VITE_*` value, which is public bundle plaintext. 5. **Managed-target and body boundaries**, SDK routes accept only the server port owned by this process manager (including validation of the `/api` proxy target). Streaming JSON is capped before parsing: Queue 8 KiB, backup/config/database query 64 KiB, and Flow/Workflow 1 MiB. Env: `AGENT_ALLOWED_ORIGINS` (comma-separated; merged with dev defaults `http://localhost:5273`, `http://127.0.0.1:5273`), `AGENT_ALLOWED_HOSTS` and `AGENT_TOKEN`; the all-in-one server also reads `TRUST_PROXY` and the separate `BUNQUEUE_TOKEN`, which gates every remote/proxied `/api/*` request. `BUNQUEUE_MANAGED=0` selects attach-only mode: the agent probes the configured `BUNQUEUE_URL` with that server token and rejects lifecycle/config mutations. It also rejects backup restore because a local child manager cannot prove that an externally supervised broker has stopped using the database. > **Reverse proxy / custom hostname on a loopback bind.** The Host allowlist is > loopback names plus `AGENT_ALLOWED_HOSTS`; the all-in-one binary also includes > hostnames from `AGENT_ALLOWED_ORIGINS`. A proxy that preserves public `Host` > must list it in either variable. A Host-rewriting proxy must list its raw > replacement Host, overwrite `X-Forwarded-Host` with the external authority, > and set `TRUST_PROXY=1`. Any such non-loopback configuration switches the > `/agent` bridge to all-route token authentication and disables it with `403` > if `AGENT_TOKEN` is missing. Merely binding the socket to loopback does not > make a public proxy local. > **The `/api` credential is separate.** Remote/proxied all-in-one deployments > also fail `/api/*` closed without `BUNQUEUE_TOKEN`; when configured, every API > request must carry it as a bearer. Enter that value as the Server token in > Settings. If Bunqueue enables `AUTH_TOKENS`, the same value must be accepted > upstream because the Authorization header is forwarded unchanged. > The plain `bun run agent` (`agent/index.ts`) direct listener intentionally > remains a zero-config loopback tool. Do **not** publish port `6800` through a > reverse proxy; use the all-in-one `/agent` bridge or put independent > authentication in front of it. Only the all-in-one binary reads `BIND_ADDR`. > For `0.0.0.0`, list every public/LAN name or IP in `AGENT_ALLOWED_HOSTS` (or > its full origin in `AGENT_ALLOWED_ORIGINS`); wildcard binds cannot infer them. ## Endpoints (`http://127.0.0.1:6800`) | Method · Path | Action | | --- | --- | | `GET /control/status` | `{ managementMode, status, generation, pid, startedAt, exitCode, healthy, version, config, runningConfig, db, ... }` (`managed` probes the child; `external` adds `reachable`, `externalUrl`, `healthStatus` and `healthError`) | | `POST /control/start` | Spawn the server, return status | | `POST /control/stop` | SIGTERM → SIGKILL, return status | | `POST /control/restart` | Stop then start | | `GET /control/logs` | `{ lines: [{ seq, ts, stream, line }] }` | | `GET /control/config` | current `ServerConfig` | | `PUT /control/config` | update config (allowed anytime; ports/data-path apply on next start/restart) | Operational endpoint families: | Prefix | Capability | | --- | --- | | `/flows/*` | Five FlowProducer creates, `getFlow`, parent results, safe Flow Job reads/mutations and bounded completion wait | | `/workflows/*` | Runtime status/reload, start, signal, recover, compensation decisions, archive/cleanup, execution reads | | `/queue-operations/*` | Live limits/TTL/saturation, deduplication, metrics, and lifecycle-journal retention | | `/backup/*` | Configure, status, list, create and stopped-server guarded restore | | `/db/*` | Read-only tables, rows, cells, schema, CSV and bounded SELECT-style query execution | See [API mapping](api-mapping.md) for each exact method, query, and safety constraint. `db` = `{ path, exists, size, walSize, shmSize, totalSize, mtimeMs }`, bytes on disk for the SQLite main file plus its WAL/SHM sidecars. ## Configuration `ServerConfig` = `{ command, httpPort, tcpPort, dataPath, extraEnv }`. Updates are validated atomically: unknown keys, an empty/non-string command, invalid ports, a non-string data path, or a non-string environment map return HTTP 400 without partially changing the previous configuration. The agent launches `command` (default `bunx bunqueue@2.9.4 start`, e.g. `bun run ../src/main.ts` when developing) with `HTTP_PORT`, `TCP_PORT`, the selected storage environment and `extraEnv` injected. PostgreSQL mode is selected by `BUNQUEUE_STORAGE_DRIVER=postgres` or `BUNQUEUE_POSTGRES_URL`; the agent removes inherited SQLite path aliases so Bunqueue 2.9 cannot receive an ambiguous PostgreSQL-plus-SQLite configuration. Config is **editable at any time**; a running process keeps its launch config (`runningConfig`) and picks up port/data-path changes on the next start/restart. Defaults come from env: `AGENT_PORT`, `BUNQUEUE_START_CMD`, `HTTP_PORT`, `TCP_PORT`, `BUNQUEUE_DATA_PATH`. ## In the dashboard `Control ▸ Server` (`pages/control/ServerControl.tsx`) polls `bq.control.status()` and `bq.control.logs()`, shows status / health / pid / uptime, a **storage row** (SQLite db / WAL / total on-disk / last-modified from `status.db`), exposes Start / Stop / Restart, an **always-editable config form** (with a `Save & restart` shortcut and a "Restart to apply changes" hint when the live config differs), and a live, colour-coded process-log tail. If the agent is unreachable it shows how to start it (`bun run agent/index.ts`). The storage row, Database inspector and S3 backup routes are SQLite-only. In PostgreSQL or in-memory mode status reports no SQLite file and those routes return `409` instead of reading or mutating an unrelated local path. ## Tested `test/manager.test.ts` starts and stops a real child process (`sleep`), asserts running/stopped transitions and pid, verifies config **can be edited while running** (and that the change only applies on restart, leaving `runningConfig` intact), checks `dbStats()` reports on-disk sizes (and reports a missing db as empty), checks stdout + system log capture (`echo`), and proves the **concurrent stop-then-start race** no longer orphans the newly-started process. `test/agent-server.test.ts` covers the security policy against synthetic requests (no port bound): the Origin allowlist / no-wildcard CORS, a cross-origin `PUT /control/config` rejected `403` **without** mutating the launch command (the CSRF-to-RCE vector), same-origin + non-browser requests succeeding, OPTIONS preflight ACAO, loopback mutation auth, and all-route auth for network exposure. `bun run test:e2e` additionally starts disposable Bunqueue 2.9.4 servers and executes every FlowProducer creation mode, every exposed safe Flow Job group, Workflow handler discovery/control/compensation/archive, all eight exposed Queue SDK operations, and the unexposed `removeDlqJob` compatibility contract. Its PostgreSQL stage starts three separately authenticated agents/brokers against one disposable PostgreSQL 18.6 namespace and proves cross-broker enqueue, leased pull/ack, queue state, cron, and rate-limit visibility. Backup worker and compiled-binary behavior are covered by the runtime-safety suite and the standalone build smoke test. The deterministic lifecycle suite suspends a Workflow request body across stop and restart and proves it cannot run after Engine closure or cross into a new process generation. `bun run test:package` then creates the npm tarball, installs it in a temporary consumer outside the repository, starts its real bin, and probes the dashboard, direct agent, and `/agent` bridge. ## Native TLS for SDK bridges Queue Operations, FlowProducer and Workflow Engine share one authenticated loopback TCP connection configuration. `TLS_CERT_FILE` or `TLS_KEY_FILE` in the managed environment enables TLS automatically. When the server command uses `--tls-cert`/`--tls-key` instead, also set `BUNQUEUE_AGENT_TCP_TLS=true`. Set `BUNQUEUE_AGENT_TCP_CA_FILE` to an absolute CA certificate path for a private CA. It also enables TLS. Certificates must cover `127.0.0.1`, the fixed connection host. Certificate verification is always enabled; invalid settings fail closed. `extraEnv` overrides inherited environment values, including an explicit empty value. Changing TLS settings recreates the persistent Workflow connection on its next use. Bunqueue native TLS also enables HTTPS. Configure the dashboard/proxy `BUNQUEUE_URL` accordingly and arrange HTTPS CA trust separately; the TCP CA setting applies only to the SDK bridges, not the browser or HTTP proxy. `bun run test:e2e:tls` creates an ephemeral certificate, runs all three bridges against a real TLS broker, and verifies rejection without the trusted CA. --- --- url: /bunqueue-dashboard/docs/api-mapping.md description: >- Every bunqueue HTTP endpoint the dashboard drives, with verified request and response shapes and the job-action state table. --- # API mapping & shape gotchas The bundled client and default server now target **2.9.4**. This release has no public API, wire-format or schema changes from 2.9.3; the historical contract notes below still describe those unchanged upstream behaviors. Runtime tests probe the server version and assert it matches the installed, pinned package. Flow payload replacement, retry, remove and removeUnprocessedChildren are now rejected at the UI transport, agent route and service boundaries. Older endpoint inventories below describe the SDK surface, not permission to execute those mutations. Explicit dependency release remains available; promotion, priority and delay are checked against the same state policy as ordinary jobs. `bq` (`src/lib/bq.ts`) targets bunqueue's HTTP API. Shapes below were verified against the exact [bunqueue v2.9.3 server release](https://github.com/egeominotti/bunqueue/releases/tag/v2.9.3) (`3fbfde2`) and the installable 2.9.3 client; several differ from older dashboard assumptions. ## Bunqueue 2.9.3 compatibility boundary The Dashboard exposes the 2.9.3 operational additions: group admission (`maxSize`), group priority, group pause/resume, bounded group-job listing and per-priority counts; completed-history retention is configurable from Server Control. SQLite is migrated to schema 37 and PostgreSQL to schema 20 by the upstream server. PostgreSQL members sharing a namespace must be upgraded as one unit, and a SQLite backup must be taken before the first 2.9.3 start because a partially started migration cannot be downgraded safely. Native Worker batches, processor `AbortSignal`, Observable processor results, and the `QueuePro`/`WorkerPro`/`QueueEventsPro` aliases are application-library contracts rather than remote operator commands. They need no separate Dashboard transport; the Dashboard continues to display the jobs, workers, events and results they produce through the same 2.9.3 server contracts. ## Fleet and connection profiles `/fleet` does not use one mutable global request while iterating nodes. It captures each profile's Bunqueue URL/token and paired agent URL/token, then issues independent `GET /health` and `GET /control/status` probes. A failure on one endpoint does not discard the other nodes. `POST /control/start|stop|restart` is sent directly to the card's captured agent identity, even when that profile is not the active Dashboard node. The agent status exposes `storageMode`, `postgresNamespace`, and a credential-free `postgresTarget` (`host:port/database`). Fleet groups only exact target + namespace matches; usernames, passwords, URL query parameters, and fragments never enter the response. Selecting a profile changes the server, agent, and both token scopes as one connection generation. ## Workflow Engine Workflow Engine is a Bunqueue client-library API, not part of the Bunqueue HTTP server surface. The local control agent combines a read-only observability adapter over the official SQLite store with a persistent official `Engine`: | Agent endpoint | Contract | | --- | --- | | `GET /workflows/stats` | Active/archive totals, active state counts, and workflow names | | `GET /workflows?kind=&workflowName=&state=&limit=&offset=` | Deterministic execution summaries, capped at 100 rows | | `GET /workflows/:id?kind=active\|archive` | Decoded input, step records, resolved paths, signals, decisions, definition/rollback metadata | | `GET /workflows/runtime?target=` | Module/handler readiness and registered workflow names | | `POST /workflows/runtime/reload?target=` | Close and recreate the Engine from `BUNQUEUE_WORKFLOW_MODULE` | | `POST /workflows/start?target=` | `{ workflowName, input }` → official `Engine.start` | | `POST /workflows/:id/signal?target=` | `{ event, payload }` → official durable `Engine.signal` | | `POST /workflows/recover?target=` | Official orphan recovery | | `POST /workflows/:id/resume-compensation?target=` | Resume a stuck saga unwind | | `POST /workflows/:id/abandon-compensation?target=` | Abandon the remaining unwind explicitly | | `POST /workflows/archive?target=` | Archive bounded terminal states by age | | `POST /workflows/cleanup?target=` | Permanently delete bounded terminal states by age | The adapter uses the same structured-clone MessagePack codec as Bunqueue 2.9.3 and opens the configured `dataPath` read-only. The published 2.9.2 npm package migrates from SQLite schema 35 and PostgreSQL schema 19; 2.9.3 targets schema 37 and 20 respectively. Mutations never edit SQLite: they execute on the live Engine loaded from an absolute application module. Target pinning, stopped-server checks, bounded payloads, terminal-only maintenance, serialization, and the agent auth/origin/host gates protect that local control plane. ## FlowProducer and Flow Job methods The Job Flows page keeps its portable HTTP DAG reader, and uses target-pinned agent routes for the TCP-only Bunqueue client contracts: | Agent endpoint | Contract | | --- | --- | | `POST /flows/create?target=` | `add`, `addBulk`, `addChain`, `addBulkThen`, or `addTree`; definitions are validated and committed by official `FlowProducer` | | `GET /flows/tree?id=&queueName=&depth=&maxChildren=&target=` | Official `FlowProducer.getFlow` snapshot; the UI exposes both bounded traversal controls and tree/raw-JSON views | | `POST /flows/results?target=` | One or many official parent results | | `GET /flows/jobs/:id/:operation?queueName=&target=` | State predicates, `toJSON`, `asJSON`, dependency/failure reads | | `GET /flows/jobs/:id/waitUntilFinished?queueName=&target=&ttl=` | Bounded 1–60,000 ms completion wait; dedicated TCP and browser deadlines use `ttl + 5,000 ms` | | `POST /flows/jobs/:id/:operation?queueName=&target=` | Progress/log/delay/priority/log retention/deduplication, dependency release and promote; payload replacement, retry and removal are rejected | Every operation first resolves the ID and queue through the official flow reader, so a caller cannot retarget a job by changing only the displayed queue. Bodies are size/type bounded and unknown options fail before a TCP connection is opened. Worker-lease transitions and process-local `discard()` remain inside the real Worker process. For `updateProgress`, numeric values preserve the optional message. Object values follow the v2.9.3 Flow Job contract (`progress: 0` plus the serialized object as the message) after strict JSON, prototype, depth, value-count, and 65,536-byte validation. ## Queue SDK operations The Bunqueue HTTP server can write rate and concurrency policies but does not expose the matching read contracts. Queue Control therefore uses a pinned local agent bridge over the official Bunqueue 2.9.3 `Queue` client: | Agent endpoint | Official Queue contracts | | --- | --- | | `GET /queue-operations/:queue/limits?target=&maxJobs=` | `getGlobalRateLimit`, `getGlobalConcurrency`, `getRateLimitTtl`, `isMaxed` | | `GET /queue-operations/:queue/groups?target=&groupId=&maxJobs=&maxCount=&start=&end=` | `getGroupJobsCount`, `getGroupsJobsCount`, `getGroupActiveCount`, `getGroupRateLimit`, `getGroupRateLimitTtl`, `getGroupConcurrency`, `isGroupPaused`, `getGroupJobs`, `getCountsPerPriorityForGroup` | | `POST /queue-operations/:queue/groups/pause?target=` | `pauseGroup` | | `POST /queue-operations/:queue/groups/resume?target=` | `resumeGroup` | | `POST /queue-operations/:queue/groups/rate-limit?target=` | `setGroupRateLimit` | | `POST /queue-operations/:queue/groups/rate-limit/remove?target=` | `removeGroupRateLimit` | | `POST /queue-operations/:queue/groups/concurrency?target=` | `setGroupConcurrency` | | `POST /queue-operations/:queue/groups/concurrency/remove?target=` | `removeGroupConcurrency` | | `GET /queue-operations/:queue/deduplication?target=&deduplicationId=` | `getDeduplicationJobId` | | `POST /queue-operations/:queue/deduplication/remove?target=` | `removeDeduplicationKey` | | `GET /queue-operations/:queue/metrics?target=&type=&start=&end=` | Paged `getMetrics` for completed or failed buckets | | `POST /queue-operations/:queue/events/trim?target=` | Bounded `trimEvents` retention mutation | The agent accepts only the managed server target, exact query/body fields, validated queue/group names, positive safe-integer group policies, inclusive group-job pages of at most 100 entries, and bounded pagination/retention values. Operations are serialized. Deduplication-key removal and journal trimming require explicit UI confirmation; trimming lifecycle events does not remove metric buckets. The remaining client methods are deliberately not operator commands: `removeDlqJob` is E2E-validated but deliberately not exposed: its queue + job ID input is not an atomic generation precondition when custom IDs can be reused. Worker-lease transitions (`extendJobLock`, `moveJobTo*`) require the owning worker token; `waitUntilReady`, `disconnect`, and `close` are adapter lifecycle; `discard` is process-local; and `forward` owns a long-lived arbitrary-destination runtime. Generic Drain/Clean/Obliterate and non-atomic retry paths remain flow-destructive and fail closed as documented below. Compatibility aliases are covered through their acknowledged remote counterparts rather than duplicated as fire-and-forget controls. ## S3 backup agent | Agent endpoint | Contract | | --- | --- | | `GET /backup/status?target=` | Official Bunqueue 2.9.3 CLI JSON status | | `GET /backup/list?target=` | Remote object list | | `POST /backup/configure?target=` | Atomically replace only whitelisted `S3_*` config keys | | `POST /backup/now?target=` | Create a consistent backup | | `POST /backup/restore?target=` | Restore only while stopped and only when the confirmed database snapshot is unchanged | Commands are serialized, time/output bounded, spawned without a shell, and run with the managed database path. The restore body includes the chosen object key plus path/existence/size/WAL/SHM/mtime evidence from `/control/status`. ## Response-shape gotchas (important) | Endpoint | Envelope | Notes | | --- | --- | --- | | `GET /storage` | `{ ok, data: { diskFull, error, since } }` | **wrapped in `data`**; no `path` field | | `GET /webhooks` | `{ ok, data: { webhooks[], stats } }` | **wrapped in `data`** | | `GET /workers` | `{ ok, data: { workers[], stats } }` | **wrapped in `data`** | | `GET /ping` | `{ ok, data: { pong, time } }` | **wrapped in `data`** | | `GET /health` | `{ ok, status, version, uptime, queues, connections, memory, storage? }` | flat; `ok` is a **health flag**. Disk-full returns the structured degraded body with **HTTP 503**, which both clients deliberately accept as diagnostic data | | `GET /queues/:q/dlq` | `{ ok, entries[], total }` | flat (no `data`) | | `GET /queues/:q/dlq/stats` | `{ ok, stats }` | flat | | `GET /crons` | `{ ok, crons[] }` | flat | | `GET /queues/:q/counts` | `{ ok, counts }` | flat | | `GET /queues/summary` | `[{ name, paused, counts:{waiting,active,completed,failed,delayed} }]` | **bare array**, no `{ ok }` envelope at all; one round-trip for every queue's full counts (see [pages.md](pages.md) / A5 in the project changelog) | * **DLQ entry** = `{ job, enteredAt, reason, error, attempts[] }`. The job is **nested**; there is no top-level `id`/`name`. Both clients use `entry.job.id` and model the attempt history as an array. * **Jobs have a first-class `name`**, separate from arbitrary user `data`, and expose **`startedAt` / `completedAt`** (not `processedOn` / `finishedOn`). Duration = `completedAt − startedAt`. Direct and list reads embed terminal `returnvalue` and `failedReason`; the dedicated `GET /jobs/:id/result` route remains available for older compatible servers. * **Job `timeline`** (`Array<{state,timestamp,worker?,error?,attempt?}>`) is pushed on every state transition (enqueue, start, complete/fail, requeue) and, despite an in-source comment suggesting otherwise, **is persisted** to SQLite as a packed blob and restored on read, capped at 20 entries (`MAX_TIMELINE_ENTRIES`). It's present for completed and DLQ'd jobs too, not just in-memory ones. * **`backoffConfig`** is `{ type: 'fixed'|'exponential', delay, maxDelay? } | null`. `null` doesn't mean "no backoff", it means the job used the plain numeric `backoff` field with the server's default strategy (exponential, `job.backoff * 2^attemptsMade`, ±50% jitter, capped at 1h). v2.9.3 accepts both numeric and structured backoff inputs. One upstream readback caveat: SQLite's list-row serializer currently restores `backoffConfig` and the deduplication detail fields as defaults, so `/jobs/list` can omit those details even though `GET /jobs/:id` still reports the live job accurately. * Job `delay` is **milliseconds, relative**; timestamps are ms. ## Strict mode: `{ ok: false }` on HTTP 200 Several mutating endpoints return **HTTP 200 even on logical failure**, with `{ ok: false, error }` in the body, cancelling a job that's already finished, purging an empty DLQ, rate-limiting an unknown queue, etc. `bq.ts`'s `call()` parses every response and throws a `BqError` when it sees `ok === false`, so these now surface as errors at the call site instead of silently resolving as success. **One deliberate exception:** `bq.health()` passes `strict:false`, because `/health`'s `ok` field means "is the server healthy" (legitimately `false` in the informative HTTP 503 disk-full response), treating that as a thrown error would break any page rendering a "degraded" state. If you add a new endpoint whose `ok` means something other than request-success, follow that pattern (`srv(path, init, false)`) rather than special-casing it in a page. `lib/api.ts` (the classic client) implements the same logical-failure check, with `strict:false` for health/storage responses whose `ok` field represents health rather than request success. ## Job action gating The upstream endpoints below still exist, but endpoint availability is not the same as dashboard authorization. `lib/jobActions.ts::actionGates(state)` is the single client-side model used by `JobInspector` and `JobsPro`; it additionally fails closed where v2.9.3 cannot prove worker or reverse-flow safety: | Action | Endpoint | Upstream scope | Dashboard exposure | | --- | --- | --- | --- | | Cancel | `DELETE /jobs/:id` | Queue-resident jobs | **Never.** Hidden reverse dependencies can be stranded. | | Discard (→ DLQ) | `POST /jobs/:id/discard` | Queue or processing location, without an expected-state precondition or terminal flow-failure resolution | **Never.** A stale runnable snapshot can become active, and a flow child can strand its parent. | | Edit data | `PUT /jobs/:id/data` | Replaces the complete payload | Waiting/delayed/prioritized non-Flow jobs only. Flow jobs are read-only because replacement would erase reserved parent/children metadata. | | Set priority / delay | `PUT /jobs/:id/priority` · `PUT /jobs/:id/delay` | Queue location (and active for some delay paths) | Waiting/delayed/prioritized only. | | Promote | `POST /jobs/:id/promote` | Delayed only | Delayed only. | | Retry active | `POST /jobs/:id/move-to-wait` | Active only | **Never.** It can duplicate side effects from the still-running worker. | | Retry from DLQ | `POST /queues/:q/dlq/retry { jobId }` | Failed/DLQ | **Never.** The separate GET + POST has no atomic job-generation, state or topology precondition; the POST can hit a different job recreated under the same ID. | | Requeue | `POST /queues/:q/retry-completed { id }` | Completed only | **Never.** Upstream `retryCompleted` does not reconstruct dependency registration or original flow order. | A logical `{ok:false}` always throws. The dashboard also refuses unsafe actions before transport, even where the upstream handler would accept them. A pinned target, exact ID and fresh empty-topology snapshot cannot authorize DLQ retry: none of those observations is an atomic condition on the later POST. The dashboard Copilot uses the same fail-closed policy. Its only mutating tools are Promote, Pause and Resume; DLQ retry and completed-job requeue are absent. ## Request bodies | Action | Method · Path | Body | | --- | --- | --- | | Add job | `POST /queues/:q/jobs` | `{ name?, data, priority?, delay?, maxAttempts?, backoff?, timeout?, jobId?, removeOnComplete?, removeOnFail?, durable?, ttl?, uniqueKey?, lifo?, tags?, groupId?, dependsOn?, repeat? }` → `{ ok, id }`. `name` defaults to `default` and is separate from user `data`. The upstream single route does not forward 2.9.3 `groupMaxSize`; when that field is set the Dashboard deliberately uses the bulk route even for one job, preventing a silent admission-limit drop. The dashboard accepts only interval repeat `{ every, limit? }`: v2.9.3's continuation path treats `pattern` as `every ?? 0`, so cron expressions must use `/crons`. The client validates and sends one captured JSON representation, preventing mutable getters or root `toJSON()` from changing repeat, IDs, dependencies or topology after preflight | | Add bulk | `POST /queues/:q/jobs/bulk` | `{ jobs: JobInput[] }` → `{ ok, ids }`; the domain shape calls a custom id `customId`, so the client translates dashboard `jobId` before sending. Bulk spec mode preserves all single-add fields plus 2.9.3 `groupMaxSize`, grouped priority (`0..2097151`), `stallTimeout`, `dedup`, `stackTraceLimit` and `timestamp`. It rejects `parentId`, `childrenIds` and the four dependency-failure flags, which require atomic Flow creation, as well as the persisted compatibility fields `keepLogs`, `sizeLimit`, `debounceId` and `debounceTtl`, which v2.9.3 does not enforce as enqueue controls. The dashboard incrementally serializes at most 10,000 jobs, caps the exact translated JSON envelope at 64 MiB, validates repeat/ID/dependency/topology safety from those captured fragments, and sends the same string so getters or `toJSON()` cannot create a second-pass bypass | | Update data | `PUT /jobs/:id/data` | `{ data }` | | Change priority | `PUT /jobs/:id/priority` | `{ priority, lifo? }` | | Change/move delay | `PUT /jobs/:id/delay` · `POST /jobs/:id/move-to-delayed` | `{ delay }` (ms) | | Fail | `POST /jobs/:id/fail` | `{ error?, unrecoverable?, stack? }` | | Clean | `POST /queues/:q/clean` | `{ grace?, state?, limit? }` → `{ ok, count }`; upstream route documented, intentionally not exposed outside session-owned Benchmark cleanup | | Promote delayed | `POST /queues/:q/promote-jobs` | `{ count? }` → `{ ok, count }` | | Retry completed | `POST /queues/:q/retry-completed` | Upstream accepts `{ id? }` → `{ ok, count }` (omitting `id` targets every completed job), but the dashboard never calls it because `retryCompleted` does not rebuild dependency registration/flow order | | Rate limit | `PUT /queues/:q/rate-limit` | `{ limit, duration?, ttl? }` | | Concurrency | `PUT /queues/:q/concurrency` | `{ concurrency }` (or `{ limit }`) | | Stall config | `PUT /queues/:q/stall-config` | `{ config: { enabled, stallInterval, maxStalls, gracePeriod } }` | | DLQ policy | `PUT /queues/:q/dlq-config` | Upstream accepts `{ config: { autoRetry, autoRetryInterval, maxAutoRetries, maxAge, maxEntries } }`. Dashboard saves omit `maxAge` and `maxEntries` because they drive destructive expiry/evacuation without an atomic target check; those fields are read-only. It may send `autoRetry:false` but rejects enabling it. | | Retry DLQ | `POST /queues/:q/dlq/retry` | Upstream accepts `{ jobId? }`; the dashboard never calls either the exact-ID or retry-all form because the mutation has no atomic generation/state/topology precondition | | Create/upsert cron | `POST /crons` | Last-writer-wins upsert `{ name, jobName?, queue, data?, schedule? \| repeatEvery?, priority?, timezone?, dedup?, jobOptions? }`; `jobName` is assigned to every spawned job and defaults to `default`; there is no atomic create-only precondition | | Add webhook | `POST /webhooks` | `{ url, events[], queue?, secret? }` (events ∈ `job.pushed/started/completed/failed/progress`) | Benchmark workers call `pull-batch` with an explicit per-run owner and require one non-empty lease token per returned job. The same tokens are forwarded to heartbeat, batch acknowledgement, and pre-ACK move-to-wait compensation. This is required by Bunqueue 2.9.3's PostgreSQL lease contract and keeps benchmark completion portable across brokers instead of relying on process-local lock ownership. ## Live stream `GET /events` (or `/events/queues/:q`), SSE. The **first frame is a handshake**, `retry: 3000` followed by `data: {"connected":true,"clientId":…}` with **no `event:` line**, so it parses with the SSE-spec default event name `"message"`, not a literal `"connected"` event (see [known-issues.md](known-issues.md) for the dashboard-side consequence). After that: `job:pushed`, `job:active`, `job:completed`, `job:failed`, `job:progress`, … plus `queue:counts` and periodic `stats:snapshot` / `health:status` system frames. Job-event payloads carry `queue`/`jobId`/ `timestamp` plus optional `error`/`progress`/`prev`/`delay`; lifecycle SSE payloads still do not include the first-class job name. `useActivityStream` maps event suffix → status and keeps a bounded buffer + counters + rolling throughput. The fetch client requires `Content-Type: text/event-stream`; a proxy fallback returning HTML/JSON with HTTP 200 is rejected instead of entering a silent reconnect loop. ## Auth If the server sets `AUTH_TOKENS`, enter a token in Settings for the current browser session; `bq`/`api` send `Authorization: Bearer `, and the SSE reader (fetch-based, not `EventSource`) sends it too. `EventSource` can't carry custom headers, which is exactly why `lib/sse.ts` exists instead of the native API. Never put the token in a `VITE_*` value: it would be plaintext in the public bundle. A remote/proxied all-in-one dashboard also requires its server-side `BUNQUEUE_TOKEN` on every `/api/*` request. Enter the same value in Settings; the proxy validates it and forwards the Authorization header unchanged. If the upstream enables `AUTH_TOKENS`, it must accept that value too. --- --- url: /bunqueue-dashboard/docs/known-issues.md description: >- A verified, non-glossed list of the bunqueue dashboard's current bugs and design constraints, each pointing at the exact file to look at. --- # Known issues Bunqueue 2.9.4 is now the installed/default target; it retains the 2.9.3 API and schemas described below. Flow safety gates now also apply to the SDK console and direct agent calls: payload replacement, retry, remove and unprocessed-child removal are blocked. Queue/Flow/Workflow bridges now support verified native TCP TLS and private CA configuration; see [agent.md](agent.md). Verified against the current source (not speculative), each entry cites the exact file so you can confirm or fix it. None of these are catastrophic; the dashboard is fully usable. They're documented here because "professional docs" means being honest about the rough edges, not hiding them. ## [Bunqueue v2.9.x](https://github.com/egeominotti/bunqueue/releases/tag/v2.9.3) server-contract constraints These constraints are in the upstream HTTP contract and cannot be made atomic by a browser client. The dashboard fails closed where it can and names the risk at the point of action: * **Every DLQ retry path is unavailable.** The server exposes a read followed by a separate `POST /queues/:q/dlq/retry`, but the POST has no atomic precondition for job generation/identity, state or topology. Pinning the server, re-reading an exact failed ID and seeing empty topology is therefore insufficient: that job can disappear and a different job can be recreated under the same ID before the POST. Manual row retry, Jobs bulk retry, queue-wide retry and Copilot retry all fail closed. * **Individual DLQ removal is unavailable.** Bunqueue 2.9's `Queue.removeDlqJob()` durably deletes records, but its input is still only queue + job ID. If a custom ID is reused between observation and deletion, it can target a different generation, so the dashboard does not expose it. * **Completed-job requeue is unavailable.** The upstream `retryCompleted` implementation resets a completed job but does not reconstruct dependency registration or the ordering guarantees of its original flow. The dashboard never calls the single-ID or queue-wide retry-completed route. * **DLQ retention is display-only.** Saving a smaller `maxEntries` can immediately evacuate existing entries, while `maxAge` drives destructive expiry; neither path has an atomic generation/topology check for what it removes. The dashboard shows both server values read-only and omits `maxAge`/`maxEntries` from every save. Existing auto-retry can only be turned off, never enabled. * **Other flow-destructive mutations are not topology-aware.** Cancel, Discard, Drain, Clean, Obliterate and DLQ Purge can delete a job that another queue still depends on. Bunqueue exposes neither reverse-dependency inspection nor an atomic conditional mutation, so those paths also fail closed. Session-owned Benchmark cleanup is the only queue-clean exception. * **Cron creation is an upsert.** `POST /crons` has no create-only/CAS precondition. The dashboard blocks names visible in the current list and runs a fresh fail-closed preflight immediately before POST, but two clients racing the same absent name can still replace one another. The form and confirmation call the command an upstream, last-writer-wins upsert and require operators to authorize that behavior for a globally unique name. * **Rate-limit and concurrency policies are write-only over HTTP.** Bunqueue's HTTP surface still exposes PUT/DELETE without matching reads, so the desired- state forms remain explicit replacements rather than editable cached values. Queue Control now complements them with live, target-pinned Bunqueue 2.9.3 Queue SDK readback for the global rate limit, concurrency, remaining TTL, and saturation; write receipts are never presented as server truth. Resolving the remaining mutation items completely requires generation/state/ topology-conditional APIs, flow-aware retry reconstruction, and create-only cron semantics in Bunqueue itself. ## MCP TCP worker ID (Bunqueue 2.9.4) The public MCP `register_worker` tool can return `success: true` and worker ID `"0"` while the real broker stores a different ID. A heartbeat with the returned ID then reports `success: false`. Read the worker registry and match a unique name and queues before heartbeat/unregister; inspect the JSON success field, not only MCP `isError`. The managed browser regression verifies the real ID, heartbeat, dashboard display and removal. See [MCP setup](/guide/mcp). ## Documentation direct URLs (fixed in 0.0.45) The theme's custom view-transition wrapper previously forwarded only the URL to VitePress's router and dropped its `initialLoad` option. A direct clean URL could return valid HTML but become a 404 during hydration. The wrapper now forwards all router arguments and skips transitions on initial load; real browser tests cover every generated page at desktop and mobile widths plus search and history. ## Copilot browser cancellation (fixed in 0.0.45) AI SDK 7.0.14 creates a telemetry completion promise even in the browser, where its non-Node early return did not attach a rejection handler. Stop during a pending tool confirmation therefore emitted an unhandled AbortError despite correctly cancelling the mutation. `patches/ai@7.0.14.patch` adds the same local promise handling used by the SDK's Node path. The package version is pinned and frozen installs apply the patch; Docker copies it before dependency installation. The real-browser regression keeps browser-error assertions enabled and proves that Stop leaves the queue unchanged. Recheck this regression before removing the patch when upgrading the SDK. ## Adversarial audit pass (v0.0.32) Every module was re-read against the invariants it assumes, each suspected defect was challenged by an independent reviewer before being accepted, and the 92 that survived were fixed with regression tests (170 → 314 tests). The full list is in [the changelog](https://github.com/egeominotti/bunqueue-dashboard/blob/main/CHANGELOG.md). What matters for operators: * **Network-facing agent access fails closed.** Non-loopback binds and reverse-proxied loopback binds require `AGENT_TOKEN`; it gates every bridged `/agent/*` route, including database, logs, config, and status reads. Remote policy is selected by the bind, `TRUST_PROXY`, forwarding headers, a public request Host, or explicit non-loopback allowed hosts/origins. Only genuinely loopback access keeps zero-configuration reads. The direct `:6800` listener stays loopback-only and is never the public bridge. * **Network-facing admin-API access fails closed.** The all-in-one server uses the same remote-policy signals for `/api/*`: without `BUNQUEUE_TOKEN` it returns `403`, and with one configured every request needs that exact bearer. Enter it as the Server token in Settings. The header is forwarded unchanged, so a Bunqueue server using `AUTH_TOKENS` must accept the same value. Static deployments do not pass through this boundary and still need upstream/front- proxy authentication. * **Wildcard Host policy is explicit.** The Host allowlist is enforced on every route, so `BIND_ADDR=0.0.0.0` must list every public/LAN name or address in `AGENT_ALLOWED_HOSTS` (origins in `AGENT_ALLOWED_ORIGINS` count too). It does not infer DHCP addresses, aliases, container service names, or Kubernetes pod IPs. * **Proxied deployments:** the `/api` and `/agent` Origin gates compare the request's **host**, not its full origin, precisely so a TLS-terminating reverse proxy (browser sends `https://…`, the binary sees `http://…`) does not 403 every mutation while read-only GETs keep working. A proxy that preserves `Host` must first admit that public name via `AGENT_ALLOWED_HOSTS` or `AGENT_ALLOWED_ORIGINS`. A proxy that **rewrites** `Host` must also admit the rewritten raw Host, overwrite `X-Forwarded-Host`, and set `TRUST_PROXY=1`; that header is ignored for Origin matching by default, because a direct caller could otherwise declare itself same-origin. Listing the exact public origin in `AGENT_ALLOWED_ORIGINS` can replace the forwarded value for Origin policy, but never bypasses validation of the raw Host. * **Coverage floors are enforced on non-`.tsx` code** and were raised to 88% lines / 89% functions, with additional per-module floors for lifecycle, authentication, configuration persistence and database execution. A JSX module enters the lcov denominator merely by being imported, so the aggregate tracked test *scope* rather than tested *behaviour*; the overall number is still reported by `scripts/check-coverage.ts`. React components remain largely uncovered by unit tests — that is a real gap, not a measurement artifact. ## Recently fixed (kept here for history) A security + gate pass resolved these, no longer present: * **SQLite deadlines now kill the work.** Database browsing, custom queries and Workflow storage reads run in bounded disposable processes. Timeouts and request aborts wait for the child to exit before releasing lifecycle leases; an IPC supervisor also terminates work after abrupt agent death. * **Server configuration survives agent restarts.** Successful changes are atomically saved to `AGENT_CONFIG_PATH` (default `.bunqueue-dashboard/config.json`). Invalid saved state fails startup explicitly; restarting does not start the broker. * **Publishing waits for the complete validation gate.** Release, npm, Docker and Pages depend on quality, all three Playwright browsers and native execution checks for the five release platforms. Release assets are the tested binaries. * **`AGENT_TOKEN` end-to-end.** The browser agent client never sent the token, so a token-protected agent 401'd every control action, and the 401 popped the wrong (server) token prompt. The client now sends the agent token, the `auth:required` event is scoped (server vs agent), and Settings has an **Agent token** field that remains in memory for the browser session. See [agent.md](agent.md). Tokens are no longer sourced from `VITE_*`, where they would be visible in the public bundle. * **DNS-rebinding read exposure closed.** The agent now enforces a **Host-header allowlist** (loopback + `AGENT_ALLOWED_HOSTS`) in addition to the Origin gate, so a page whose DNS was rebound to loopback can no longer read `/control/*` or `/db/*` over Origin-less same-origin GETs. The standalone binary applies the same gate to `/api`, `/agent` and assets on loopback and network binds. * **Benchmark accounting is run-scoped.** Worker runs first require an empty dedicated per-tab queue, track the exact ids returned by every producer batch, and only ACK/count those ids. If another producer races the preflight, its jobs are returned to waiting and the benchmark stops. The UI does not expose a drain-only mode for arbitrary queues. * **Alert channel secrets no longer persisted.** `alertsStore` kept `webhook`/ `slack` targets (secret URLs) in `localStorage`; they're now memory-only. * **`agent/` and `scripts/` are typechecked** by the build gate (`tsconfig.agent.json`), whereas the npm bin and agent code used to ship with no typecheck. A performance + pagination pass resolved these, no longer present: * **Per-poll fan-outs collapsed.** OverviewPro (was 8 req/poll), MetricsPro (was 22), DlqPro (was N+2) now issue 2 to 3 requests per poll via `GET /queues/summary`; JobsPro is single-queue server-paginated (was up to 25 `jobs/list` per poll). Classic Jobs refuses its all-queue mode above 100 queues rather than issuing up to 10,000 periodic requests; every permitted pool pins one target and is lifecycle-cancelled. `usePolledData` is now self-scheduling (at most one fetch in flight, no pile-ups) and **pauses while the tab is hidden**, except the **first** fetch, which always runs (same for `useThroughputSeries`'s first sample): a page opened in a background tab used to sit on "Loading…" (and the sidebar on "connecting") until focused. * **Every list is paginated**, see the `Pagination` component in [components.md](components.md). Server-paginated where the API supports it (queues, DLQ via offset/limit/total; jobs via offset/limit + `hasNext`), client-paginated for full-list endpoints (crons, webhooks, workers, activity). * **`usePolledData` race fixed** with a generation guard (last-to-START wins). * **MetricsPro/`Metrics` latency** now reads the real nested per-operation percentiles (`push`/`pull`/`ack` × p50/p95/p99) instead of always-0. * **Uptime** no longer rendered ~1000× too large (ms→s) on OverviewPro/MetricsPro. * **Topbar titles** now cover all Control routes (no more "bunqueue · bunqueue"). * **`bq.call()`** now throws on HTTP-200-with-`{ok:false}` (except `health()`), so failed cancel/purge/retry surface as errors instead of false success. * **Responsive**: fluid root type (`clamp()`), a mobile nav drawer + hamburger, responsive padding. See [components.md](components.md). A stability re-check resolved these, no longer present: * **`StatusBadge` no longer crashes on a missing/undefined `status`.** `status.toLowerCase()` had no guard; two nav-reachable callers (`LogsPro.tsx`, `JobTimeline.tsx`) passed `e.status`/`e.state` with no fallback, unlike `JobsPro`/`JobInspector` which already defaulted to `'waiting'`. Fixed at the root (`StatusBadge.tsx`) so all six callers are covered: empty/undefined status now renders as "unknown" instead of throwing. * **`AreaChart` no longer blanks on a `NaN`/`Infinity` point.** `finite()` sanitizes each value to `0` before it reaches the max computation or the SVG path builder, so one bad point degrades to a dip instead of corrupting every series' path data. ## UI/UX pass (this change-set) A four-auditor UI/UX sweep (52 findings) was applied on top of the stability sweep. Highlights: theme-aware semantic status colors (`text-success/warning/ danger`, the dark-palette 400 shades failed WCAG AA on the light theme),`Field` now wires label→input (`useId`), focus-visible rings across the shell and kit, standardized `{ok,text}` green/red action feedback on every mutating control, destructive confirms name their target and counts (Clean had NO confirm), Enter submits the create forms, honest empty states when a filter, not the data, is empty, `live={!error}` on Metrics/Diagnostics, DLQ job IDs link to the Job Inspector, `/usage` and `/workers` graduated to Pro pages (`/cron` now serves CronManager; classics remain at `*-classic`), Settings buffers the server URL (was retargeting all polling per keystroke), and a new **Database** section: read-only SQLite inspector (agent-side `readonly` connection, tables, schema/indexes/DDL, sortable grid, query runner with history/EXPLAIN/CSV/JSON export). `scripts/dev.ts` now spawns services directly instead of via `bun run` wrappers, which did not forward SIGTERM and were the root cause of the recurring orphaned vite/agent processes. ## Database inspector, standalone timeout fixed * **Compiled binaries embed the supervised SQLite reader.** The standalone build includes `scripts/serve.ts`, `agent/dbReadWorker.ts` and the backup worker. The executable re-executes itself to isolate each read, so `/db/query` keeps the same enforceable 5-second deadline as `bun start`. Queries remain read-only, statement-allowlisted and capped at 500 rows in every distribution mode. ## Stability sweep (adversarially verified, earlier change-set) A multi-dimension bug hunt (every finding independently verified by refute / reproduce / impact passes before fixing) resolved the following, gate green, with regression tests where practical (`test/format.test.ts`, `test/manager.test.ts`): * **Live activity feed no longer misorders bursts under StrictMode.** The `setEvents` updater in `useActivityStream` mutated its captured batch via `.reverse()`, an impure updater React invokes twice in dev, flipping a multi-event flush back to the wrong order. The reverse now happens once, outside the updater. * **`formatDuration` can no longer render "1m 60s" / "60.0s"**, the remainder is derived from a single up-front rounding (119,700 ms → "2m 0s"). * **JobInspector**: a failed lookup now distinguishes 404 ("Job not found", URL param cleared so the deep-link effect can't silently re-load the previous job over the failure) from network/5xx errors (real message shown; a valid job is no longer reported as "removed" when the server is merely down). * **JobsPro / DlqPro / DlqControl stale-view race fixed** (QueueControl's tagging pattern): after switching queue/filter/page, the previous view's rows can no longer stay rendered, with live action buttons, under the new selection, so Retry/Cancel can't fire against the wrong entity. * **JobDataEditor no longer wipes unsaved edits** on every action-driven job reload, it re-seeds by content, not object identity. Flow jobs are read-only: v2.9.3 replaces the full payload and would otherwise erase the reserved parent/children metadata used by FlowReader. * **ServerControl shows an amber "agent unreachable" banner** (and disables lifecycle buttons, freezes the uptime ticker) when the status poll fails after a successful one, it used to keep asserting "Running / healthy" with a live-ticking uptime for a dead agent. * **Agent orphan fix**: `agent/index.ts` now handles SIGINT/SIGTERM and stops the managed bunqueue server before exiting (Ctrl-C on `bun start` used to leave it running, holding :6790 and the SQLite db). `scripts/dev.ts` waits 10s (was 2s) so the agent's SIGTERM→SIGKILL escalation can complete. * **Agent log pipe flushes the final unterminated chunk**, a crash cause written without a trailing newline used to vanish from Process Logs. * **Agent spawn-failure race**: a `start()` whose spawn throws while a stale `stop()` is finalizing now clears `proc`/`runningConfig` (status no longer reports a dead pid + launch config for a stopped server). * **ErrorBoundary resets on ANY navigation** (`location.key`, was pathname only), re-clicking the crashed section's nav item or navigating between `/job?id=X` variants now recovers instead of appearing permanently broken. * **CopyButton**: falls back to `execCommand('copy')` on insecure (plain-HTTP) origins, the documented Docker deployment, and flashes a red ✕ on failure instead of silently doing nothing. * **Theme flash fixed**: an inline pre-paint script in `index.html` applies the persisted light theme before the bundle loads (was a dark→light flash on every visit). * **`react-router` joined the `react-vendor` chunk** (the manualChunks regex missed it, in React Router 7 it holds the whole router; `react-router-dom` is a shim), so app-only deploys no longer re-download the router. * **Standalone binary proxy fixed**: `scripts/serve.ts` now strips `content-encoding`/`content-length`/`transfer-encoding` from proxied responses (Bun's fetch decompresses bodies but kept the headers, behind any gzip proxy every `/api` response failed with `ERR_CONTENT_DECODING_FAILED`), and missing `/assets/*` files 404 (matching the Docker image's Caddy) instead of returning index.html to a stale chunk import. * **`strictPort: true`**: Vite now fails fast when :5273 is taken instead of silently serving on :5274 while `bun start`'s banner points at the stale instance. * **docker.yml / pages.yml now run the full gate** (lint + build + test) before publishing, a commit rejected by CI could previously still ship as `edge` / to the public Pages site. ## Audit fix pass (earlier change-set) A full-component adversarial audit fixed the following. Each was verified, then fixed with the gate (build + lint + `bun test`) green; the agent + store fixes ship with reproducing tests (`test/agent-server.test.ts`, `test/manager.test.ts`, `test/sse.test.ts`, `test/s3store.test.ts`). * **Control agent is no longer unauthenticated-RCE-by-design.** `agent/` now enforces an **Origin allowlist** and **locked CORS** (never `*`, ACAO is reflected only for allowed origins) and rejects any request carrying a disallowed `Origin` (403) before it reaches the `ProcessManager`. A malicious tab's cross-origin `PUT /control/config` → `POST /control/start` can no longer set + run a command. Non-browser callers (curl) still work. Set `AGENT_TOKEN` for an extra bearer-token gate on state-changing requests; configure allowed origins via `AGENT_ALLOWED_ORIGINS`. Handler logic is factored into `agent/server.ts` (unit-tested). See [agent.md](agent.md). * **Agent stop/start race fixed.** A `stop()` awaiting an old process could orphan a process a concurrent `start()` brought up (manager reported "stopped" while a server was still running). `ProcessManager` now guards every `onExit`/`stop()` mutation by a monotonic process token. * **`ConfigForms` cross-queue write fixed.** `StallForm`/`DlqConfigForm` now `useEffect(() => setC(config), [config])` and `QueueControl` renders them with `key={queue}`, so switching queue no longer saves queue A's stall/DLQ config onto queue B. Save now surfaces errors inline (was silent + unhandled rejection). * **`useActivityStream` connection indicator fixed.** `connected` now flips true on *any* delivered frame (the handshake carries `data.connected` with no `event:` line), so an idle-but-live queue no longer shows "Connecting…" forever; and the stream now **auto-reconnects** (2s backoff) after a clean end / server restart instead of going silently dead. * **`OverviewPro` banner reflects connection loss.** After the first successful poll, a later failure now shows an amber "Connection lost, showing last known data / Stale" banner instead of a permanent green "Online" over frozen numbers. Recent Activity rows now show the real `queue`/`jobId` (were all "unnamed"). * **`setRateLimit` now actually applies.** `api.setRateLimit` sent `{max,duration}` but the server reads `{limit}`, so the classic `QueueDetail` rate-limit control silently no-op'd while showing "Saved". It now sends `{limit}`; the dead "Duration (ms)" input was removed. `api.ts`'s `request()` now also throws on HTTP-200-`{ok:false}` (except `storage()`/`health()`), matching `bq.call()`. * **S3 secret no longer persisted.** `s3Store` uses `partialize` to keep `accessKeyId`/`secretAccessKey` in memory only, they are no longer written to `localStorage` in plaintext. * **`AddJob` bulk-with-custom-ID** now reports the real created count (`new Set(ids).size`) and caps/validates `Count` (≤10000). * **`DlqControl`** no longer fetches unused `dlqStats` (whose failure blanked the whole page). **`DlqPro`** keeps the `Pagination` control mounted when a page-scoped reason/search filter matches nothing (was a navigation trap), and labels its page-scoped sort honestly on multi-page queues. * **`QueueDetail` Recent Jobs** now shows real Name (`data.name`) and Duration (`startedAt`/`completedAt`) instead of "unknown" or a placeholder dash. * **`Workers`** surfaces a "showing first 100 of N" hint when the list is truncated. **`ServerControl`** validates ports (1 to 65535, HTTP≠TCP) before restart. **`Topbar`** guards `decodeURIComponent` (malformed URL no longer crashes the shell). **`useThroughputSeries`** has an in-flight guard (no overlapping polls). **`Webhooks`** enable/disable toggle has an accessible name. * **App-wide `ErrorBoundary`.** `src/components/ErrorBoundary.tsx` wraps the whole shell, so a single render throw shows a recoverable fallback instead of blanking the entire app. ## Classic-page correctness pass * **Storage and DLQ shapes now match the live API.** The classic client reads `/storage` from `{ ok, data }`, and `DlqEntry` uses the nested `{ job, enteredAt, reason, error, attempts[] }` shape. The classic S3 page no longer masks disk-full, and a non-empty classic DLQ renders instead of crashing or showing blank identifiers. * **Classic timestamps and totals are accurate.** Overview/Usage convert uptime milliseconds before formatting, Jobs uses `startedAt`/`completedAt`, and Queues gets its header totals from the global dashboard summary rather than the current page. * **Legacy Jobs and Logs no longer advertise unavailable data.** Jobs reads an optional display name from job data, renders Cancel unavailable under the flow-safety policy, and refreshes the queue list every 30 seconds. Logs shows the SSE event type instead of a permanently unknown job name. ## UX gaps * \~~**`src/pages/Alerts.tsx` is fully built but unreachable.**~~ **Fixed:** the Alerts page is now routed at `/alerts` with a Monitoring nav item, and a client-side engine (`src/lib/useAlertEngine.ts`, mounted app-wide via `AlertEngine`) evaluates the enabled rules against live metrics. ## Design limitations (not bugs, how bunqueue OSS works) * **S3 operations require the local control agent.** `/s3` can now apply the whitelisted Bunqueue environment, inspect/list backups, create one on demand, and perform a stop-gated, snapshot-confirmed restore through the exact 2.9.3 CLI. `/s3-classic` remains a read-only environment reference. Static hosting and arbitrary remote targets cannot run commands on a machine they do not manage. * **Alerts are evaluated client-side, with real limits.** `useAlertEngine` now evaluates the rules in the browser (in-app toast + optional desktop Notification on each fresh threshold crossing), but: (1) it only runs **while a tab is open** (even backgrounded), so it is not away-from-desk paging; the email/webhook/slack **delivery channels still have no backend** (bunqueue OSS has no alerting engine, so wire them into your own monitoring or hosted bunqueue Cloud); (2) the **`p99_latency`** metric is **global only**, because bunqueue exposes latency percentiles keyed by TCP operation (push/pull/ack), not per queue, so a queue-scoped p99 rule evaluates the global max operation p99, not that queue's job latency. * **Multiple pages cover overlapping ground on purpose** (three DLQ pages, two cron pages, `-classic` duplicates), this is the additive convention from `CLAUDE.md`, not accidental drift. See [pages.md](pages.md#sidebar-page-mapping).