@mayurbhusare/resilient-queue-ui 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 resilient-queue
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,205 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # @mayurbhusare/resilient-queue-ui
2
+
3
+ An administration dashboard and REST API adapter for [`@mayurbhusare/resilient-queue`](https://www.npmjs.com/package/@mayurbhusare/resilient-queue).
4
+
5
+ Provides real-time queue observability, metrics inspection, Dead-Letter Queue (DLQ) exploration, and deep job diagnostics with zero framework lock-in.
6
+
7
+ ---
8
+
9
+ ## Features
10
+
11
+ - **Real-Time Queue Telemetry**: Inspect waiting, active, delayed, and dead-letter job counts in real time via atomic queue metrics.
12
+ - **Dead-Letter Queue (DLQ) Explorer**: Browse and inspect failed jobs, complete with failure reasons, retry counts, stall metadata, and corrupted JSON resilience.
13
+ - **Deep Job Diagnostics**: Search and inspect individual job descriptors by ID, displaying execution progress (0–100%), timestamps, data payloads, and return values.
14
+ - **Framework-Agnostic Adapters**: Mount the dashboard seamlessly into existing **Express** or **Fastify** applications, or run it as a standalone monitoring server.
15
+ - **Distributed Safety**: Supports multi-queue configurations and distributed Redis deployments without requiring invasive hooks in worker processes.
16
+
17
+ ---
18
+
19
+ ## Installation
20
+
21
+ Install `@mayurbhusare/resilient-queue-ui` alongside its peer dependency, `@mayurbhusare/resilient-queue`:
22
+
23
+ ```bash
24
+ # npm
25
+ npm install @mayurbhusare/resilient-queue-ui @mayurbhusare/resilient-queue
26
+
27
+ # yarn
28
+ yarn add @mayurbhusare/resilient-queue-ui @mayurbhusare/resilient-queue
29
+
30
+ # pnpm
31
+ pnpm add @mayurbhusare/resilient-queue-ui @mayurbhusare/resilient-queue
32
+ ```
33
+
34
+ ### Runtime & Peer Dependency Requirements
35
+
36
+ | Requirement | Supported Version Range |
37
+ | :--- | :--- |
38
+ | **Node.js** | `>=18.0.0` (LTS 18, 20, 22) |
39
+ | [`@mayurbhusare/resilient-queue`](https://www.npmjs.com/package/@mayurbhusare/resilient-queue) | `^2.2.0` |
40
+
41
+ ---
42
+
43
+ ## Integration Examples
44
+
45
+ The package is designed with a decoupled architecture where core dashboard dispatchers are wrapped by lightweight framework adapters.
46
+
47
+ ### 1. Express Integration
48
+
49
+ ```javascript
50
+ const express = require("express");
51
+ const { ResilientQueue } = require("@mayurbhusare/resilient-queue");
52
+ const { createExpressAdapter } = require("@mayurbhusare/resilient-queue-ui");
53
+
54
+ const app = express();
55
+
56
+ const emailQueue = new ResilientQueue({ redisUrl: "redis://127.0.0.1:6379", keyPrefix: "rq:emails" });
57
+ const videoQueue = new ResilientQueue({ redisUrl: "redis://127.0.0.1:6379", keyPrefix: "rq:video" });
58
+
59
+ // Host application authentication (MANDATORY)
60
+ app.use("/admin/queues", authMiddleware);
61
+
62
+ // Mount dashboard UI & REST API
63
+ app.use(
64
+ "/admin/queues",
65
+ createExpressAdapter({
66
+ queues: {
67
+ "emails": emailQueue,
68
+ "video-transcoding": videoQueue
69
+ }
70
+ })
71
+ );
72
+
73
+ app.listen(3000, () => {
74
+ console.log("Admin dashboard active at http://localhost:3000/admin/queues");
75
+ });
76
+ ```
77
+
78
+ ### 2. Fastify Integration
79
+
80
+ ```javascript
81
+ const fastify = require("fastify")();
82
+ const { ResilientQueue } = require("@mayurbhusare/resilient-queue");
83
+ const { fastifyDashboardPlugin } = require("@mayurbhusare/resilient-queue-ui");
84
+
85
+ const orderQueue = new ResilientQueue({ redisUrl: "redis://127.0.0.1:6379", keyPrefix: "rq:orders" });
86
+
87
+ // Register dashboard plugin under a prefix with auth hook
88
+ fastify.register(fastifyDashboardPlugin, {
89
+ prefix: "/admin/queues",
90
+ queues: {
91
+ "orders": orderQueue
92
+ },
93
+ preHandler: fastifyAuthHook
94
+ });
95
+
96
+ fastify.listen({ port: 3000 });
97
+ ```
98
+
99
+ ---
100
+
101
+ ## Dashboard UI & Frontend Architecture
102
+
103
+ The package includes a built-in, lightweight administration dashboard served directly by the Express and Fastify adapters:
104
+
105
+ - **Clean, Responsive Layout**: Includes a sidebar navigation area and responsive drawer for mobile devices.
106
+ - **Views**:
107
+ - **Queue Overview**: Consumes `GET /api/queues` with real-time aggregate telemetry cards, paused/active/closed/unavailable/error badges, client-side search and status filtering, multi-column sorting (with strict non-zero distinction for unavailable metrics), direct row-level navigation to DLQ Explorer and Job Inspector, and configurable background polling.
108
+ - **Dead Letter Queue (DLQ) Explorer**: Read-only browser for dead-letter jobs with pagination, sort order toggle, corrupted JSON resilience, failure diagnostics, payload inspection, and cross-linking to Job Inspector.
109
+ - **Job Inspector**: Comprehensive job inspection view supporting lookups by queue and job ID (`GET /api/queues/:queueName/jobs/:jobId`), displaying execution state badges, progress bar, lifecycle timestamps, failure diagnostics, input data payloads, and return values.
110
+ - **Operational Diagnostics**: Actionable, structured guidance for known API error codes (`QUEUE_CLOSED`, `QUEUE_NOT_FOUND`, `JOB_NOT_FOUND`, `NOT_SUPPORTED`, `VALIDATION_ERROR`, `INTERNAL_ERROR`), clearly distinguishing core library version compatibility disclosures from missing queues, closed queue instances, and server errors.
111
+ - **Robust UI States**: Handles loading spinners, empty states (with server configuration snippets), empty search filter states, and temporary sync failure banners with cached queue retention.
112
+ - **Accessibility & Keyboard Usability**:
113
+ - **Full Keyboard Operability**: Complete keyboard accessibility across search filters, sort headers, drawer navigation, and table actions with high-contrast `:focus-visible` focus rings.
114
+ - **Skip to Main Content**: Accessible skip link (`#content-container`) enables assistive technology users to bypass repetitive navigation.
115
+ - **Discrete Screen-Reader Announcements**: Dedicated polite ARIA live announcer (`#live-announcer`) provides non-intrusive status updates during navigation and metric refreshes without DOM chattering.
116
+ - **Color-Independent Status**: Badges pair semantic colors with distinct visual glyphs (`●`, `⏸`, `■`, `⚠️`, `—`) and explicit ARIA labels so status is never conveyed by color alone.
117
+ - **Mobile Drawer Usability**: Includes focus trapping inside the mobile navigation menu, backdrop tap-to-dismiss, and Escape key dismissal with focus restoration to the trigger button.
118
+ - **Focus & Cursor Preservation**: Preserves active focus, input text, and cursor selection ranges across background metric auto-refresh cycles.
119
+ - **Reduced Motion Support**: Honored via `@media (prefers-reduced-motion: reduce)` to freeze animated spinners and disable non-essential transitions.
120
+ - **Zero External Frontend Dependencies**: Pure semantic HTML5, modern CSS3 with CSS variables, and modular vanilla JavaScript.
121
+
122
+ ### Reusable Frontend API Client
123
+
124
+ You can use the frontend API client directly in Node.js, bundlers, or the browser:
125
+
126
+ ```javascript
127
+ const { ApiClient, ApiError } = require("@mayurbhusare/resilient-queue-ui");
128
+
129
+ const client = new ApiClient({
130
+ baseUrl: "http://localhost:3000/admin/queues"
131
+ });
132
+
133
+ // Fetch all registered queues and metrics
134
+ const { queues } = await client.getQueues();
135
+
136
+ // Fetch metrics for a specific queue
137
+ const metrics = await client.getQueueMetrics("emails");
138
+
139
+ // Fetch DLQ entries with pagination
140
+ const dlq = await client.getQueueDLQ("emails", { start: 0, limit: 25, order: "desc" });
141
+
142
+ // Fetch individual job descriptor
143
+ const { job } = await client.getJob("emails", "job-123");
144
+ ```
145
+
146
+ ### Core Compatibility & Version Support
147
+
148
+ > [!NOTE]
149
+ > - **Full Live Functionality (`>=2.2.0`)**: Real-time telemetry metrics, DLQ exploration, pause/resume detection, and deep job inspection via `queue.getJob(jobId)` are fully supported with [`@mayurbhusare/resilient-queue@>=2.2.0`](https://www.npmjs.com/package/@mayurbhusare/resilient-queue).
150
+ > - **Legacy Compatibility (`<2.2.0`)**: If paired with an earlier core release lacking `getJob()` (such as `v2.0.2`), the dashboard adapter gracefully returns `501 NOT_SUPPORTED` and the UI renders a compatibility notice without crashing, while all other dashboard features (Overview telemetry, DLQ Explorer, and HTTP adapters) remain fully operational.
151
+
152
+ ---
153
+
154
+ ## Testing & Development
155
+
156
+ Run the complete test suite:
157
+
158
+ ```bash
159
+ # Run all tests (unit, integration, and frontend)
160
+ npm test
161
+
162
+ # Run frontend tests (ApiClient and UI views)
163
+ npm run test:frontend
164
+
165
+ # Run unit tests
166
+ npm run test:unit
167
+
168
+ # Run adapter integration tests
169
+ npm run test:integration
170
+ ```
171
+
172
+ ## Security Considerations
173
+
174
+ > [!CAUTION]
175
+ > **Host Authentication & Authorization Required**:
176
+ > Job payloads (`job.data`), execution results (`job.result`), and error diagnostics (`job.failureReason`) may contain sensitive user information, credentials, financial details, or internal system errors.
177
+
178
+ 1. **No Built-in Authentication or Authorization**: The package provides zero built-in authentication, user models, or permission schemes. Host applications **must** guard both static UI routes and `/api/*` endpoints with authentication middleware.
179
+ 2. **Apply Authentication Barriers**: Protect all dashboard routes behind your application's existing authentication stack (e.g., Express sessions/JWT, Fastify `preHandler` hooks, or reverse-proxy authentication such as OAuth2-Proxy / Cloudflare Access / AWS ALB).
180
+ 3. **Queue-Level Authorization**: If different teams or tenants share the same service, apply queue-level authorization in host middleware to restrict specific queues by user role.
181
+ 4. **CORS is NOT Authentication**: Permissive CORS headers (e.g. `Access-Control-Allow-Origin: *`) do **not** protect or authenticate endpoints. Never enable unauthenticated cross-origin access to dashboard routes.
182
+ 5. **Caching Protection**: All API endpoints emit `Cache-Control: no-store` and `X-Content-Type-Options: nosniff` to prevent proxy or browser caching of sensitive job data.
183
+ 6. **Credential Redaction**: Internal Redis and database connection URIs (e.g., `redis://user:pass@host:port`) and auth tokens in 500 internal error responses are automatically scrubbed and redacted to `[redacted]`.
184
+ 7. **Never Expose Publicly**: Never mount `@mayurbhusare/resilient-queue-ui` routes on an unauthenticated, publicly accessible endpoint. Bind management processes to private VPC networks or loopback interfaces where possible.
185
+
186
+ For full architectural details, threat models, Express and Fastify integration patterns, and the production checklist, see the [Security Guide (docs/security.md)](./docs/security.md) and runnable [Examples (examples/)](./examples/README.md).
187
+
188
+ ---
189
+
190
+ ## API Specification
191
+
192
+ The REST API contract powering the dashboard is detailed in [docs/api-contract.md](./docs/api-contract.md).
193
+
194
+ ---
195
+
196
+ ## Architecture & Portability
197
+
198
+ - **No Root Coupling**: This package maintains independent dependencies, configuration, and documentation, allowing seamless extraction into a dedicated repository.
199
+ - **Strict Public API Usage**: The package interacts with `@mayurbhusare/resilient-queue` exclusively through its published public interface (`import { ResilientQueue } from "@mayurbhusare/resilient-queue"`), avoiding any reliance on internal submodule paths.
200
+
201
+ ---
202
+
203
+ ## License
204
+
205
+ [MIT](./LICENSE) &copy; 2026 Resilient Queue.
package/package.json CHANGED
@@ -1,6 +1,57 @@
1
1
  {
2
2
  "name": "@mayurbhusare/resilient-queue-ui",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0",
4
+ "description": "Standalone administration dashboard and API adapter for @mayurbhusare/resilient-queue",
5
+ "main": "src/index.js",
6
+ "exports": {
7
+ ".": "./src/index.js",
8
+ "./api-client": "./src/ui/api-client.js",
9
+ "./ui/*": "./src/ui/*",
10
+ "./package.json": "./package.json"
11
+ },
12
+ "engines": {
13
+ "node": ">=18.0.0"
14
+ },
15
+ "type": "commonjs",
16
+ "license": "MIT",
17
+ "author": "Mayur S. Bhusare",
18
+ "keywords": [
19
+ "redis",
20
+ "queue",
21
+ "dashboard",
22
+ "admin",
23
+ "ui",
24
+ "monitoring",
25
+ "resilient-queue",
26
+ "job-queue"
27
+ ],
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/resilient-queue/resilient-queue-ui.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/resilient-queue/resilient-queue-ui/issues"
34
+ },
35
+ "homepage": "https://github.com/resilient-queue/resilient-queue-ui#readme",
36
+ "publishConfig": {
37
+ "access": "public"
38
+ },
39
+ "files": [
40
+ "src"
41
+ ],
42
+ "scripts": {
43
+ "dev": "node examples/dev-server.js",
44
+ "test": "node --test",
45
+ "test:frontend": "node --test tests/unit/api-client.test.js tests/unit/ui-components.test.js",
46
+ "test:unit": "node --test tests/unit/*.test.js",
47
+ "test:integration": "node --test tests/integration/*.test.js",
48
+ "test:browser": "node --test tests/e2e/*.test.js",
49
+ "test:e2e": "node --test tests/e2e/*.test.js"
50
+ },
51
+ "peerDependencies": {
52
+ "@mayurbhusare/resilient-queue": "^2.2.0"
53
+ },
54
+ "devDependencies": {
55
+ "@mayurbhusare/resilient-queue": "^2.2.0"
56
+ }
57
+ }
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+
3
+ const {
4
+ createDashboardDispatcher,
5
+ createErrorResponse,
6
+ sanitizeErrorMessage,
7
+ sendJson
8
+ } = require("../dispatcher");
9
+
10
+ /**
11
+ * Creates an Express / Connect middleware adapter for the dashboard.
12
+ *
13
+ * @param {import("../dispatcher").DashboardAdapterOptions} options
14
+ * @returns {function(req: object, res: object, next: function): void}
15
+ */
16
+ function createExpressAdapter(options) {
17
+ const dispatcher = createDashboardDispatcher(options);
18
+
19
+ return function expressAdapter(req, res, next) {
20
+ dispatcher.dispatch(req, res, next).catch((err) => {
21
+ if (typeof next === "function") {
22
+ next(err);
23
+ } else {
24
+ sendJson(
25
+ res,
26
+ 500,
27
+ createErrorResponse(
28
+ "INTERNAL_ERROR",
29
+ sanitizeErrorMessage(err?.message)
30
+ )
31
+ );
32
+ }
33
+ });
34
+ };
35
+ }
36
+
37
+ module.exports = {
38
+ createExpressAdapter
39
+ };
@@ -0,0 +1,192 @@
1
+ "use strict";
2
+
3
+ const {
4
+ createDashboardDispatcher,
5
+ resolveUiAssetPath,
6
+ sendStaticFile,
7
+ sanitizeErrorMessage
8
+ } = require("../dispatcher");
9
+
10
+ /**
11
+ * Fastify plugin adapter for @mayurbhusare/resilient-queue-ui.
12
+ *
13
+ * @param {object} fastify Fastify instance
14
+ * @param {object} options Plugin options
15
+ * @param {function} [done] Callback invoked when plugin registration completes
16
+ */
17
+ function fastifyDashboardPlugin(fastify, options = {}, done) {
18
+ const dispatcher = createDashboardDispatcher(options);
19
+
20
+ const routeOptions = {};
21
+ if (
22
+ options &&
23
+ (typeof options.preHandler === "function" ||
24
+ Array.isArray(options.preHandler))
25
+ ) {
26
+ routeOptions.preHandler = options.preHandler;
27
+ }
28
+
29
+ fastify.get("/api/queues", routeOptions, async (request, reply) => {
30
+ try {
31
+ const result = await dispatcher.executeQueuesRoute();
32
+ reply
33
+ .header("Cache-Control", "no-store")
34
+ .header("X-Content-Type-Options", "nosniff")
35
+ .code(result.status)
36
+ .send(result.body);
37
+ } catch (err) {
38
+ reply
39
+ .header("Cache-Control", "no-store")
40
+ .header("X-Content-Type-Options", "nosniff")
41
+ .code(500)
42
+ .send({
43
+ error: {
44
+ code: "INTERNAL_ERROR",
45
+ message: sanitizeErrorMessage(err?.message || "Internal server error")
46
+ }
47
+ });
48
+ }
49
+ });
50
+
51
+ fastify.get(
52
+ "/api/queues/:queueName/metrics",
53
+ routeOptions,
54
+ async (request, reply) => {
55
+ try {
56
+ const rawQueueName = request?.params?.queueName;
57
+ const result = await dispatcher.executeMetricsRoute(rawQueueName);
58
+ reply
59
+ .header("Cache-Control", "no-store")
60
+ .header("X-Content-Type-Options", "nosniff")
61
+ .code(result.status)
62
+ .send(result.body);
63
+ } catch (err) {
64
+ reply
65
+ .header("Cache-Control", "no-store")
66
+ .header("X-Content-Type-Options", "nosniff")
67
+ .code(500)
68
+ .send({
69
+ error: {
70
+ code: "INTERNAL_ERROR",
71
+ message: sanitizeErrorMessage(err?.message || "Internal server error")
72
+ }
73
+ });
74
+ }
75
+ }
76
+ );
77
+
78
+ fastify.get(
79
+ "/api/queues/:queueName/dlq",
80
+ routeOptions,
81
+ async (request, reply) => {
82
+ try {
83
+ const rawQueueName = request?.params?.queueName;
84
+ const query = request?.query || {};
85
+ const result = await dispatcher.executeDLQRoute(rawQueueName, query);
86
+ reply
87
+ .header("Cache-Control", "no-store")
88
+ .header("X-Content-Type-Options", "nosniff")
89
+ .code(result.status)
90
+ .send(result.body);
91
+ } catch (err) {
92
+ reply
93
+ .header("Cache-Control", "no-store")
94
+ .header("X-Content-Type-Options", "nosniff")
95
+ .code(500)
96
+ .send({
97
+ error: {
98
+ code: "INTERNAL_ERROR",
99
+ message: sanitizeErrorMessage(err?.message || "Internal server error")
100
+ }
101
+ });
102
+ }
103
+ }
104
+ );
105
+
106
+ fastify.get(
107
+ "/api/queues/:queueName/jobs/:jobId",
108
+ routeOptions,
109
+ async (request, reply) => {
110
+ try {
111
+ const rawQueueName = request?.params?.queueName;
112
+ const rawJobId = request?.params?.jobId;
113
+ const result = await dispatcher.executeJobRoute(rawQueueName, rawJobId);
114
+ reply
115
+ .header("Cache-Control", "no-store")
116
+ .header("X-Content-Type-Options", "nosniff")
117
+ .code(result.status)
118
+ .send(result.body);
119
+ } catch (err) {
120
+ reply
121
+ .header("Cache-Control", "no-store")
122
+ .header("X-Content-Type-Options", "nosniff")
123
+ .code(500)
124
+ .send({
125
+ error: {
126
+ code: "INTERNAL_ERROR",
127
+ message: sanitizeErrorMessage(err?.message || "Internal server error")
128
+ }
129
+ });
130
+ }
131
+ }
132
+ );
133
+
134
+ const uiRoutes = [
135
+ "/",
136
+ "/index.html",
137
+ "/styles.css",
138
+ "/api-client.js",
139
+ "/app.js",
140
+ "/views/overview.js",
141
+ "/views/dlq.js",
142
+ "/views/job.js",
143
+ "/ui",
144
+ "/ui/",
145
+ "/ui/index.html",
146
+ "/ui/styles.css",
147
+ "/ui/api-client.js",
148
+ "/ui/app.js",
149
+ "/ui/views/overview.js",
150
+ "/ui/views/dlq.js",
151
+ "/ui/views/job.js"
152
+ ];
153
+
154
+ for (const route of uiRoutes) {
155
+ fastify.get(route, routeOptions, async (request, reply) => {
156
+ try {
157
+ const asset = resolveUiAssetPath(route);
158
+ if (asset) {
159
+ sendStaticFile(reply, asset.fullPath, asset.contentType);
160
+ } else {
161
+ reply
162
+ .header("Cache-Control", "no-store")
163
+ .header("X-Content-Type-Options", "nosniff")
164
+ .code(404)
165
+ .send({ error: { code: "ROUTE_NOT_FOUND", message: "Route not found" } });
166
+ }
167
+ } catch (err) {
168
+ reply
169
+ .header("Cache-Control", "no-store")
170
+ .header("X-Content-Type-Options", "nosniff")
171
+ .code(500)
172
+ .send({
173
+ error: {
174
+ code: "INTERNAL_ERROR",
175
+ message: sanitizeErrorMessage(err?.message || "Internal server error")
176
+ }
177
+ });
178
+ }
179
+ });
180
+ }
181
+
182
+ if (typeof done === "function") {
183
+ done();
184
+ return;
185
+ }
186
+
187
+ return Promise.resolve();
188
+ }
189
+
190
+ module.exports = {
191
+ fastifyDashboardPlugin
192
+ };