@pulse-labs/server-pulse 0.0.0-stage → 0.0.1

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/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.0.1] - 2026-10-14
10
+
11
+ First public preview on npm as `@pulse-labs/server-pulse`. Scaffold only: the middleware is a pass-through. (Briefly published as `@mukesh-singh9862/server-pulse`, which was unpublished.)
12
+
13
+ ### Added
14
+
15
+ - Project scaffold: TypeScript build (tsup, CJS + ESM + types), Vitest 4, ESLint, Prettier, CI workflow.
16
+ - `npm run smoke`: runner-free smoke test of the built package (also runs on Node 18.15 in CI).
17
+ - Pass-through `serverPulse()` middleware with the public API surface.
18
+ - `examples/express4-basic`.
19
+ - MIT license; published publicly to npm under the `@pulse-labs` scope.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 pulse-labs
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,94 @@
1
- # Temporary Holding Version
1
+ # server-pulse
2
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.
3
+ Live server monitoring dashboard for Node.js apps, served from your own base URL (for example `https://api.example.com/monitor`).
4
+
5
+ > **Status:** early preview (`0.0.x`, milestone M0 — project scaffold). The middleware is currently a pass-through and does not collect anything yet. Do not use it in production before `1.0.0`. Sections marked _TODO_ are completed in later milestones.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @pulse-labs/server-pulse
11
+ ```
12
+
13
+ Requires Node.js 18.15 or newer. The package has no runtime dependencies.
14
+
15
+ ## Quick start (Express 4 / 5)
16
+
17
+ ```js
18
+ const express = require('express');
19
+ const { serverPulse } = require('@pulse-labs/server-pulse');
20
+
21
+ const app = express();
22
+ const pulse = serverPulse({
23
+ auth: { username: process.env.PULSE_USER, password: process.env.PULSE_PASS },
24
+ });
25
+ app.use(pulse); // must be the FIRST middleware
26
+ // ... your routes ...
27
+ app.use(pulse.errorHandler()); // optional: captures error messages for "Recent errors"
28
+ app.listen(3000);
29
+ ```
30
+
31
+ Open `http://localhost:3000/monitor`.
32
+
33
+ ESM / TypeScript: `import { serverPulse } from '@pulse-labs/server-pulse';`
34
+
35
+ ### Mounting rules
36
+
37
+ - Register `app.use(pulse)` at the application root, before any other middleware — especially before `compression()`, because compression buffers Server-Sent Events.
38
+ - Do not mount it under a sub-path (`app.use('/x', pulse)` is unsupported). Use the `basePath` option instead.
39
+
40
+ ## NestJS
41
+
42
+ _TODO (M12)._
43
+
44
+ ## PM2 cluster
45
+
46
+ _TODO (M9). The monitor process is started with `./node_modules/@pulse-labs/server-pulse/dist/cli.cjs monitor --port 9100`._
47
+
48
+ ## Options
49
+
50
+ _TODO (M2)._
51
+
52
+ ## Environment variables
53
+
54
+ _TODO (M2)._
55
+
56
+ ## Nginx
57
+
58
+ _TODO._
59
+
60
+ ## Security notes
61
+
62
+ _TODO._
63
+
64
+ ## Troubleshooting
65
+
66
+ _TODO._
67
+
68
+ ## Development
69
+
70
+ ```bash
71
+ npm install
72
+ npm run build # tsup + copy src/ui to dist/ui
73
+ npm test # vitest
74
+ npm run lint
75
+ npm run typecheck
76
+ npm run smoke # smoke test of the built package
77
+ ```
78
+
79
+ ## Publishing
80
+
81
+ Published publicly to npm as `@pulse-labs/server-pulse` (MIT).
82
+
83
+ ```bash
84
+ npm login # once per machine
85
+ npm whoami # your account must be a member of the pulse-labs npm organization
86
+ npm pack --dry-run # review the files that will be published
87
+ npm publish # runs lint, typecheck and tests first (prepublishOnly)
88
+ ```
89
+
90
+ Next release: `npm version patch|minor|major` (updates package.json and creates a git tag), update CHANGELOG.md, then `npm publish`. A published version number can never be reused, even after unpublishing.
91
+
92
+ ## License
93
+
94
+ MIT, see [LICENSE](LICENSE).
package/dist/cli.cjs ADDED
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+
4
+ // package.json
5
+ var version = "0.0.1";
6
+
7
+ // src/cli.ts
8
+ var USAGE = `server-pulse ${version}
9
+
10
+ Usage:
11
+ server-pulse monitor [--port 9100] [--host 127.0.0.1]
12
+ `;
13
+ function main(argv) {
14
+ const [command] = argv;
15
+ if (command === "--version" || command === "-v") {
16
+ process.stdout.write(version + "\n");
17
+ return 0;
18
+ }
19
+ if (command === "monitor") {
20
+ process.stderr.write("server-pulse: the monitor command is not implemented yet.\n");
21
+ return 1;
22
+ }
23
+ process.stdout.write(USAGE);
24
+ return command === void 0 || command === "--help" || command === "-h" ? 0 : 1;
25
+ }
26
+ process.exitCode = main(process.argv.slice(2));
27
+ //# sourceMappingURL=cli.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../package.json","../src/cli.ts"],"sourcesContent":["{\n \"name\": \"@pulse-labs/server-pulse\",\n \"version\": \"0.0.1\",\n \"license\": \"MIT\",\n \"description\": \"Live server monitoring dashboard for Node.js apps, served from your own base URL.\",\n \"engines\": {\n \"node\": \">=18.15\"\n },\n \"type\": \"commonjs\",\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.mjs\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"import\": {\n \"types\": \"./dist/index.d.mts\",\n \"default\": \"./dist/index.mjs\"\n },\n \"require\": {\n \"types\": \"./dist/index.d.ts\",\n \"default\": \"./dist/index.cjs\"\n }\n },\n \"./package.json\": \"./package.json\"\n },\n \"bin\": {\n \"server-pulse\": \"dist/cli.cjs\"\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"CHANGELOG.md\"\n ],\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"scripts\": {\n \"build\": \"tsup && node scripts/copy-ui.mjs\",\n \"prepare\": \"npm run build\",\n \"prepublishOnly\": \"npm run lint && npm run typecheck && npm test\",\n \"dev\": \"tsup --watch\",\n \"lint\": \"eslint .\",\n \"format\": \"prettier --write .\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"smoke\": \"node scripts/smoke-dist.mjs\",\n \"test:pm2\": \"vitest run test/integration/pm2.test.ts\",\n \"test:ui\": \"node test/ui/run.mjs\",\n \"bench\": \"node bench/overhead.mjs\"\n },\n \"peerDependencies\": {\n \"pm2\": \">=5\"\n },\n \"peerDependenciesMeta\": {\n \"pm2\": {\n \"optional\": true\n }\n },\n \"devDependencies\": {\n \"@eslint/js\": \"^9.39.5\",\n \"@types/express\": \"^5.0.6\",\n \"@types/node\": \"^20.19.43\",\n \"@types/supertest\": \"^7.2.1\",\n \"@vitest/coverage-v8\": \"^4.1.11\",\n \"eslint\": \"^9.39.5\",\n \"express4\": \"npm:express@^4.22.3\",\n \"express5\": \"npm:express@^5.3.0\",\n \"globals\": \"^17.13.0\",\n \"prettier\": \"^3.9.10\",\n \"supertest\": \"^7.3.1\",\n \"tsup\": \"^8.5.1\",\n \"typescript\": \"~5.9.3\",\n \"typescript-eslint\": \"^8.71.2\",\n \"vite\": \"^6.4.4\",\n \"vitest\": \"^4.1.11\"\n },\n \"overrides\": {\n \"tsup\": {\n \"esbuild\": \"^0.28.2\",\n \"bundle-require\": {\n \"esbuild\": \"^0.28.2\"\n }\n }\n },\n \"keywords\": [\n \"monitoring\",\n \"dashboard\",\n \"express\",\n \"nestjs\",\n \"pm2\",\n \"metrics\",\n \"sse\"\n ]\n}\n","#!/usr/bin/env node\n// `server-pulse monitor [--port 9100] [--host 127.0.0.1]` — the PM2 monitor process (spec 11.4).\n// M0 scaffold: argument handling only; the monitor itself is implemented in M9.\nimport { version } from '../package.json';\n\nconst USAGE = `server-pulse ${version}\n\nUsage:\n server-pulse monitor [--port 9100] [--host 127.0.0.1]\n`;\n\nfunction main(argv: string[]): number {\n const [command] = argv;\n if (command === '--version' || command === '-v') {\n process.stdout.write(version + '\\n');\n return 0;\n }\n if (command === 'monitor') {\n process.stderr.write('server-pulse: the monitor command is not implemented yet.\\n');\n return 1;\n }\n process.stdout.write(USAGE);\n return command === undefined || command === '--help' || command === '-h' ? 0 : 1;\n}\n\nprocess.exitCode = main(process.argv.slice(2));\n"],"mappings":";;;;AAEE,cAAW;;;ACGb,IAAM,QAAQ,gBAAgB,OAAO;AAAA;AAAA;AAAA;AAAA;AAMrC,SAAS,KAAK,MAAwB;AACpC,QAAM,CAAC,OAAO,IAAI;AAClB,MAAI,YAAY,eAAe,YAAY,MAAM;AAC/C,YAAQ,OAAO,MAAM,UAAU,IAAI;AACnC,WAAO;AAAA,EACT;AACA,MAAI,YAAY,WAAW;AACzB,YAAQ,OAAO,MAAM,6DAA6D;AAClF,WAAO;AAAA,EACT;AACA,UAAQ,OAAO,MAAM,KAAK;AAC1B,SAAO,YAAY,UAAa,YAAY,YAAY,YAAY,OAAO,IAAI;AACjF;AAEA,QAAQ,WAAW,KAAK,QAAQ,KAAK,MAAM,CAAC,CAAC;","names":[]}
package/dist/cli.d.mts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.mjs ADDED
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env node
2
+ var __getOwnPropNames = Object.getOwnPropertyNames;
3
+ var __esm = (fn, res, err) => function __init() {
4
+ if (err) throw err[0];
5
+ try {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ } catch (e) {
8
+ throw err = [e], e;
9
+ }
10
+ };
11
+ var __commonJS = (cb, mod) => function __require() {
12
+ try {
13
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
14
+ } catch (e) {
15
+ throw mod = 0, e;
16
+ }
17
+ };
18
+
19
+ // node_modules/tsup/assets/esm_shims.js
20
+ import path from "path";
21
+ import { fileURLToPath } from "url";
22
+ var init_esm_shims = __esm({
23
+ "node_modules/tsup/assets/esm_shims.js"() {
24
+ "use strict";
25
+ }
26
+ });
27
+
28
+ // package.json
29
+ var version;
30
+ var init_package = __esm({
31
+ "package.json"() {
32
+ version = "0.0.1";
33
+ }
34
+ });
35
+
36
+ // src/cli.ts
37
+ var require_cli = __commonJS({
38
+ "src/cli.ts"() {
39
+ init_esm_shims();
40
+ init_package();
41
+ var USAGE = `server-pulse ${version}
42
+
43
+ Usage:
44
+ server-pulse monitor [--port 9100] [--host 127.0.0.1]
45
+ `;
46
+ function main(argv) {
47
+ const [command] = argv;
48
+ if (command === "--version" || command === "-v") {
49
+ process.stdout.write(version + "\n");
50
+ return 0;
51
+ }
52
+ if (command === "monitor") {
53
+ process.stderr.write("server-pulse: the monitor command is not implemented yet.\n");
54
+ return 1;
55
+ }
56
+ process.stdout.write(USAGE);
57
+ return command === void 0 || command === "--help" || command === "-h" ? 0 : 1;
58
+ }
59
+ process.exitCode = main(process.argv.slice(2));
60
+ }
61
+ });
62
+ export default require_cli();
63
+ //# sourceMappingURL=cli.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../node_modules/tsup/assets/esm_shims.js","../package.json","../src/cli.ts"],"sourcesContent":["// Shim globals in esm bundle\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\nconst getFilename = () => fileURLToPath(import.meta.url)\nconst getDirname = () => path.dirname(getFilename())\n\nexport const __dirname = /* @__PURE__ */ getDirname()\nexport const __filename = /* @__PURE__ */ getFilename()\n","{\n \"name\": \"@pulse-labs/server-pulse\",\n \"version\": \"0.0.1\",\n \"license\": \"MIT\",\n \"description\": \"Live server monitoring dashboard for Node.js apps, served from your own base URL.\",\n \"engines\": {\n \"node\": \">=18.15\"\n },\n \"type\": \"commonjs\",\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.mjs\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"import\": {\n \"types\": \"./dist/index.d.mts\",\n \"default\": \"./dist/index.mjs\"\n },\n \"require\": {\n \"types\": \"./dist/index.d.ts\",\n \"default\": \"./dist/index.cjs\"\n }\n },\n \"./package.json\": \"./package.json\"\n },\n \"bin\": {\n \"server-pulse\": \"dist/cli.cjs\"\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"CHANGELOG.md\"\n ],\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"scripts\": {\n \"build\": \"tsup && node scripts/copy-ui.mjs\",\n \"prepare\": \"npm run build\",\n \"prepublishOnly\": \"npm run lint && npm run typecheck && npm test\",\n \"dev\": \"tsup --watch\",\n \"lint\": \"eslint .\",\n \"format\": \"prettier --write .\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"smoke\": \"node scripts/smoke-dist.mjs\",\n \"test:pm2\": \"vitest run test/integration/pm2.test.ts\",\n \"test:ui\": \"node test/ui/run.mjs\",\n \"bench\": \"node bench/overhead.mjs\"\n },\n \"peerDependencies\": {\n \"pm2\": \">=5\"\n },\n \"peerDependenciesMeta\": {\n \"pm2\": {\n \"optional\": true\n }\n },\n \"devDependencies\": {\n \"@eslint/js\": \"^9.39.5\",\n \"@types/express\": \"^5.0.6\",\n \"@types/node\": \"^20.19.43\",\n \"@types/supertest\": \"^7.2.1\",\n \"@vitest/coverage-v8\": \"^4.1.11\",\n \"eslint\": \"^9.39.5\",\n \"express4\": \"npm:express@^4.22.3\",\n \"express5\": \"npm:express@^5.3.0\",\n \"globals\": \"^17.13.0\",\n \"prettier\": \"^3.9.10\",\n \"supertest\": \"^7.3.1\",\n \"tsup\": \"^8.5.1\",\n \"typescript\": \"~5.9.3\",\n \"typescript-eslint\": \"^8.71.2\",\n \"vite\": \"^6.4.4\",\n \"vitest\": \"^4.1.11\"\n },\n \"overrides\": {\n \"tsup\": {\n \"esbuild\": \"^0.28.2\",\n \"bundle-require\": {\n \"esbuild\": \"^0.28.2\"\n }\n }\n },\n \"keywords\": [\n \"monitoring\",\n \"dashboard\",\n \"express\",\n \"nestjs\",\n \"pm2\",\n \"metrics\",\n \"sse\"\n ]\n}\n","#!/usr/bin/env node\n// `server-pulse monitor [--port 9100] [--host 127.0.0.1]` — the PM2 monitor process (spec 11.4).\n// M0 scaffold: argument handling only; the monitor itself is implemented in M9.\nimport { version } from '../package.json';\n\nconst USAGE = `server-pulse ${version}\n\nUsage:\n server-pulse monitor [--port 9100] [--host 127.0.0.1]\n`;\n\nfunction main(argv: string[]): number {\n const [command] = argv;\n if (command === '--version' || command === '-v') {\n process.stdout.write(version + '\\n');\n return 0;\n }\n if (command === 'monitor') {\n process.stderr.write('server-pulse: the monitor command is not implemented yet.\\n');\n return 1;\n }\n process.stdout.write(USAGE);\n return command === undefined || command === '--help' || command === '-h' ? 0 : 1;\n}\n\nprocess.exitCode = main(process.argv.slice(2));\n"],"mappings":";;;;;;;;;;;;;;;;;;;AACA,OAAO,UAAU;AACjB,SAAS,qBAAqB;AAF9B;AAAA;AAAA;AAAA;AAAA;;;ACAA,IAEE;AAFF;AAAA;AAEE,cAAW;AAAA;AAAA;;;ACFb;AAAA;AAAA;AAGA;AAEA,QAAM,QAAQ,gBAAgB,OAAO;AAAA;AAAA;AAAA;AAAA;AAMrC,aAAS,KAAK,MAAwB;AACpC,YAAM,CAAC,OAAO,IAAI;AAClB,UAAI,YAAY,eAAe,YAAY,MAAM;AAC/C,gBAAQ,OAAO,MAAM,UAAU,IAAI;AACnC,eAAO;AAAA,MACT;AACA,UAAI,YAAY,WAAW;AACzB,gBAAQ,OAAO,MAAM,6DAA6D;AAClF,eAAO;AAAA,MACT;AACA,cAAQ,OAAO,MAAM,KAAK;AAC1B,aAAO,YAAY,UAAa,YAAY,YAAY,YAAY,OAAO,IAAI;AAAA,IACjF;AAEA,YAAQ,WAAW,KAAK,QAAQ,KAAK,MAAM,CAAC,CAAC;AAAA;AAAA;","names":[]}
package/dist/index.cjs ADDED
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ serverPulse: () => serverPulse
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+ var import_node_os = __toESM(require("os"));
37
+
38
+ // package.json
39
+ var version = "0.0.1";
40
+
41
+ // src/index.ts
42
+ var DEFAULT_THRESHOLDS = {
43
+ cpuPercent: [70, 85],
44
+ memoryPercent: [75, 90],
45
+ diskPercent: [80, 90],
46
+ heapPercent: [70, 85],
47
+ eventLoopLagMs: [50, 100],
48
+ errorRatePercent: [1, 3]
49
+ };
50
+ var BUCKETS_WIRE = [5, 10, 25, 50, 75, 100, 150, 250, 350, 500, 750, 1e3, 1500, 2500, 5e3, 1e4, 3e4, null];
51
+ function serverPulse(_options) {
52
+ const startedAt = Date.now();
53
+ const middleware = ((_req, _res, next) => {
54
+ next();
55
+ });
56
+ middleware.errorHandler = () => (err, _req, _res, next) => next(err);
57
+ middleware.captureError = () => {
58
+ };
59
+ middleware.snapshot = () => ({
60
+ meta: {
61
+ version,
62
+ title: import_node_os.default.hostname(),
63
+ host: import_node_os.default.hostname(),
64
+ node: process.version,
65
+ platform: process.platform,
66
+ mode: "memory",
67
+ workers: 1,
68
+ appStartedAt: Math.round(Date.now() - process.uptime() * 1e3),
69
+ monitorStartedAt: startedAt,
70
+ cores: import_node_os.default.availableParallelism?.() ?? import_node_os.default.cpus().length
71
+ },
72
+ config: {
73
+ buckets: [...BUCKETS_WIRE],
74
+ defaultTargetMs: 500,
75
+ apdexThresholdMs: 300,
76
+ thresholds: structuredClone(DEFAULT_THRESHOLDS),
77
+ windowSeconds: 900,
78
+ user: ""
79
+ },
80
+ totals: { requests: 0, since: startedAt },
81
+ history: [],
82
+ routes: [],
83
+ workers: [],
84
+ errors: []
85
+ });
86
+ middleware.stop = async () => {
87
+ };
88
+ Object.defineProperty(middleware, "mode", { value: "disabled", enumerable: true });
89
+ return middleware;
90
+ }
91
+ // Annotate the CommonJS export names for ESM import in node:
92
+ 0 && (module.exports = {
93
+ serverPulse
94
+ });
95
+ //# sourceMappingURL=index.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../package.json"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\nimport os from 'node:os';\nimport type { ServerPulseOptions, Thresholds } from './options';\nimport type { InitPayload, TickPayload } from './engine/payloads';\nimport { version } from '../package.json';\n\nexport interface ServerPulseMiddleware {\n (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void): void;\n /** Express error middleware that records the error, then calls next(err). */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n errorHandler(): (err: unknown, req: any, res: any, next: (err?: unknown) => void) => void;\n /** Record an error manually (e.g. from a NestJS exception filter). */\n captureError(err: unknown, req?: IncomingMessage): void;\n /** Current snapshot (same shape as the SSE init payload). For tests and scripts. */\n snapshot(): InitPayload;\n /** Stop timers, close SSE connections, disable event loop monitoring. */\n stop(): Promise<void>;\n /** Resolved mode: 'memory' | 'pm2' | 'disabled'. */\n readonly mode: 'memory' | 'pm2' | 'disabled';\n}\n\n// M0 scaffold: pass-through middleware. Collection, dashboard and PM2 mode arrive in later milestones.\nconst DEFAULT_THRESHOLDS: Thresholds = {\n cpuPercent: [70, 85],\n memoryPercent: [75, 90],\n diskPercent: [80, 90],\n heapPercent: [70, 85],\n eventLoopLagMs: [50, 100],\n errorRatePercent: [1, 3],\n};\n\nconst BUCKETS_WIRE = [5, 10, 25, 50, 75, 100, 150, 250, 350, 500, 750, 1000, 1500, 2500, 5000, 10000, 30000, null];\n\nexport function serverPulse(_options?: ServerPulseOptions): ServerPulseMiddleware {\n const startedAt = Date.now();\n\n const middleware = ((_req: IncomingMessage, _res: ServerResponse, next: (err?: unknown) => void) => {\n next();\n }) as ServerPulseMiddleware;\n\n middleware.errorHandler = () => (err, _req, _res, next) => next(err);\n middleware.captureError = () => {};\n middleware.snapshot = () => ({\n meta: {\n version,\n title: os.hostname(),\n host: os.hostname(),\n node: process.version,\n platform: process.platform,\n mode: 'memory',\n workers: 1,\n appStartedAt: Math.round(Date.now() - process.uptime() * 1000),\n monitorStartedAt: startedAt,\n cores: os.availableParallelism?.() ?? os.cpus().length,\n },\n config: {\n buckets: [...BUCKETS_WIRE],\n defaultTargetMs: 500,\n apdexThresholdMs: 300,\n thresholds: structuredClone(DEFAULT_THRESHOLDS),\n windowSeconds: 900,\n user: '',\n },\n totals: { requests: 0, since: startedAt },\n history: [],\n routes: [],\n workers: [],\n errors: [],\n });\n middleware.stop = async () => {};\n Object.defineProperty(middleware, 'mode', { value: 'disabled', enumerable: true });\n\n return middleware;\n}\n\nexport type { ServerPulseOptions, InitPayload, TickPayload };\n","{\n \"name\": \"@pulse-labs/server-pulse\",\n \"version\": \"0.0.1\",\n \"license\": \"MIT\",\n \"description\": \"Live server monitoring dashboard for Node.js apps, served from your own base URL.\",\n \"engines\": {\n \"node\": \">=18.15\"\n },\n \"type\": \"commonjs\",\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.mjs\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"import\": {\n \"types\": \"./dist/index.d.mts\",\n \"default\": \"./dist/index.mjs\"\n },\n \"require\": {\n \"types\": \"./dist/index.d.ts\",\n \"default\": \"./dist/index.cjs\"\n }\n },\n \"./package.json\": \"./package.json\"\n },\n \"bin\": {\n \"server-pulse\": \"dist/cli.cjs\"\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"CHANGELOG.md\"\n ],\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"scripts\": {\n \"build\": \"tsup && node scripts/copy-ui.mjs\",\n \"prepare\": \"npm run build\",\n \"prepublishOnly\": \"npm run lint && npm run typecheck && npm test\",\n \"dev\": \"tsup --watch\",\n \"lint\": \"eslint .\",\n \"format\": \"prettier --write .\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"smoke\": \"node scripts/smoke-dist.mjs\",\n \"test:pm2\": \"vitest run test/integration/pm2.test.ts\",\n \"test:ui\": \"node test/ui/run.mjs\",\n \"bench\": \"node bench/overhead.mjs\"\n },\n \"peerDependencies\": {\n \"pm2\": \">=5\"\n },\n \"peerDependenciesMeta\": {\n \"pm2\": {\n \"optional\": true\n }\n },\n \"devDependencies\": {\n \"@eslint/js\": \"^9.39.5\",\n \"@types/express\": \"^5.0.6\",\n \"@types/node\": \"^20.19.43\",\n \"@types/supertest\": \"^7.2.1\",\n \"@vitest/coverage-v8\": \"^4.1.11\",\n \"eslint\": \"^9.39.5\",\n \"express4\": \"npm:express@^4.22.3\",\n \"express5\": \"npm:express@^5.3.0\",\n \"globals\": \"^17.13.0\",\n \"prettier\": \"^3.9.10\",\n \"supertest\": \"^7.3.1\",\n \"tsup\": \"^8.5.1\",\n \"typescript\": \"~5.9.3\",\n \"typescript-eslint\": \"^8.71.2\",\n \"vite\": \"^6.4.4\",\n \"vitest\": \"^4.1.11\"\n },\n \"overrides\": {\n \"tsup\": {\n \"esbuild\": \"^0.28.2\",\n \"bundle-require\": {\n \"esbuild\": \"^0.28.2\"\n }\n }\n },\n \"keywords\": [\n \"monitoring\",\n \"dashboard\",\n \"express\",\n \"nestjs\",\n \"pm2\",\n \"metrics\",\n \"sse\"\n ]\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,qBAAe;;;ACCb,cAAW;;;ADoBb,IAAM,qBAAiC;AAAA,EACrC,YAAY,CAAC,IAAI,EAAE;AAAA,EACnB,eAAe,CAAC,IAAI,EAAE;AAAA,EACtB,aAAa,CAAC,IAAI,EAAE;AAAA,EACpB,aAAa,CAAC,IAAI,EAAE;AAAA,EACpB,gBAAgB,CAAC,IAAI,GAAG;AAAA,EACxB,kBAAkB,CAAC,GAAG,CAAC;AACzB;AAEA,IAAM,eAAe,CAAC,GAAG,IAAI,IAAI,IAAI,IAAI,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAM,MAAM,MAAM,KAAM,KAAO,KAAO,IAAI;AAE1G,SAAS,YAAY,UAAsD;AAChF,QAAM,YAAY,KAAK,IAAI;AAE3B,QAAM,cAAc,CAAC,MAAuB,MAAsB,SAAkC;AAClG,SAAK;AAAA,EACP;AAEA,aAAW,eAAe,MAAM,CAAC,KAAK,MAAM,MAAM,SAAS,KAAK,GAAG;AACnE,aAAW,eAAe,MAAM;AAAA,EAAC;AACjC,aAAW,WAAW,OAAO;AAAA,IAC3B,MAAM;AAAA,MACJ;AAAA,MACA,OAAO,eAAAA,QAAG,SAAS;AAAA,MACnB,MAAM,eAAAA,QAAG,SAAS;AAAA,MAClB,MAAM,QAAQ;AAAA,MACd,UAAU,QAAQ;AAAA,MAClB,MAAM;AAAA,MACN,SAAS;AAAA,MACT,cAAc,KAAK,MAAM,KAAK,IAAI,IAAI,QAAQ,OAAO,IAAI,GAAI;AAAA,MAC7D,kBAAkB;AAAA,MAClB,OAAO,eAAAA,QAAG,uBAAuB,KAAK,eAAAA,QAAG,KAAK,EAAE;AAAA,IAClD;AAAA,IACA,QAAQ;AAAA,MACN,SAAS,CAAC,GAAG,YAAY;AAAA,MACzB,iBAAiB;AAAA,MACjB,kBAAkB;AAAA,MAClB,YAAY,gBAAgB,kBAAkB;AAAA,MAC9C,eAAe;AAAA,MACf,MAAM;AAAA,IACR;AAAA,IACA,QAAQ,EAAE,UAAU,GAAG,OAAO,UAAU;AAAA,IACxC,SAAS,CAAC;AAAA,IACV,QAAQ,CAAC;AAAA,IACT,SAAS,CAAC;AAAA,IACV,QAAQ,CAAC;AAAA,EACX;AACA,aAAW,OAAO,YAAY;AAAA,EAAC;AAC/B,SAAO,eAAe,YAAY,QAAQ,EAAE,OAAO,YAAY,YAAY,KAAK,CAAC;AAEjF,SAAO;AACT;","names":["os"]}
@@ -0,0 +1,244 @@
1
+ import { IncomingMessage, ServerResponse } from 'node:http';
2
+
3
+ /** Warning / danger thresholds used for colours and the health badge (spec 4.3). */
4
+ interface Thresholds {
5
+ /** Default [70, 85]. */
6
+ cpuPercent: [warn: number, danger: number];
7
+ /** Default [75, 90]. */
8
+ memoryPercent: [warn: number, danger: number];
9
+ /** Default [80, 90]. */
10
+ diskPercent: [warn: number, danger: number];
11
+ /** Default [70, 85] (of heap_size_limit). */
12
+ heapPercent: [warn: number, danger: number];
13
+ /** Default [50, 100] (p99 per second). */
14
+ eventLoopLagMs: [warn: number, danger: number];
15
+ /** Default [1, 3] (5xx share). */
16
+ errorRatePercent: [warn: number, danger: number];
17
+ }
18
+ interface AuthCommon {
19
+ sessionSecret?: string;
20
+ sessionTtlMinutes?: number;
21
+ secureCookie?: 'auto' | boolean;
22
+ }
23
+ interface ServerPulseOptions {
24
+ /** Turn everything off (middleware becomes a pass-through). Default: true. Env: PULSE_ENABLED=false */
25
+ enabled?: boolean;
26
+ /** URL path of the dashboard. Default: '/monitor'. Must start with '/', no trailing slash. */
27
+ basePath?: string;
28
+ /** 'auto' | 'memory' | 'pm2'. Default: 'auto' (section 11.1). Env: PULSE_MODE */
29
+ mode?: 'auto' | 'memory' | 'pm2';
30
+ /**
31
+ * Dashboard credentials. Either one user, a list of users, or false.
32
+ * Env fallbacks: PULSE_USER, PULSE_PASS, PULSE_SECRET.
33
+ * false is only allowed when NODE_ENV !== 'production'.
34
+ */
35
+ auth?: ({
36
+ username: string;
37
+ password: string;
38
+ } & AuthCommon) | ({
39
+ users: Array<{
40
+ username: string;
41
+ password: string;
42
+ }>;
43
+ } & AuthCommon) | false;
44
+ /** Only these client IPs may reach the dashboard (exact IPv4/IPv6 or IPv4 CIDR). Others get 404. Default: allow all. */
45
+ ipAllowList?: string[];
46
+ /** Trust X-Forwarded-For / X-Forwarded-Proto (set true behind Nginx or a load balancer). Default: false. */
47
+ trustProxy?: boolean;
48
+ /** Extra requests to exclude from statistics. basePath is always excluded. */
49
+ ignore?: Array<string | RegExp | ((req: IncomingMessage) => boolean)>;
50
+ /** Apply the default ignore list (/health, /favicon.ico, static files). Default: true. */
51
+ ignoreDefaults?: boolean;
52
+ routes?: {
53
+ /** Per-route response-time targets in ms, keyed "METHOD /normalized/path". */
54
+ targets?: Record<string, number>;
55
+ /** Target for routes not listed above. Default: 500. */
56
+ defaultTargetMs?: number;
57
+ /** Maximum distinct routes tracked; extra routes go to "OTHER". Default: 500. */
58
+ maxRoutes?: number;
59
+ /** Custom normalizer; return null to fall back to the built-in one. */
60
+ normalize?: (req: IncomingMessage) => string | null;
61
+ };
62
+ /** Apdex threshold T in ms. Default: 300. */
63
+ apdexThresholdMs?: number;
64
+ /** Warning / danger thresholds used for colours and the health badge (section 4.3). */
65
+ thresholds?: Partial<Thresholds>;
66
+ /** Path used for the disk metric. Default: '/' (Windows: drive of process.cwd()). */
67
+ diskPath?: string;
68
+ pm2?: {
69
+ /** Monitor process host and port. Defaults: '127.0.0.1', 9100. Env: PULSE_MONITOR_HOST, PULSE_MONITOR_PORT */
70
+ monitorHost?: string;
71
+ monitorPort?: number;
72
+ };
73
+ ui?: {
74
+ /** Title shown in the header. Default: os.hostname(). */
75
+ title?: string;
76
+ /** Load IBM Plex Sans from Google Fonts. Default: false (system font stack, no external requests). */
77
+ webFonts?: boolean;
78
+ };
79
+ /** Optional health endpoint, outside basePath, no auth. Default: false. */
80
+ health?: false | {
81
+ path?: string;
82
+ checks?: Record<string, () => boolean | Promise<boolean>>;
83
+ };
84
+ /** Logger. Default: console with "[server-pulse]" prefix. */
85
+ logger?: Pick<Console, 'info' | 'warn' | 'error'>;
86
+ }
87
+
88
+ /** Histogram counts, length 18, in BUCKETS order. */
89
+ type Hist = number[];
90
+ /** One second of aggregated data (spec 7.1). */
91
+ interface Tick {
92
+ /** Epoch ms, start of the second. */
93
+ t: number;
94
+ req: {
95
+ /** Finished requests in this second. */
96
+ total: number;
97
+ c2: number;
98
+ c3: number;
99
+ c4: number;
100
+ c5: number;
101
+ aborted: number;
102
+ /** Value at the end of the second. */
103
+ inFlight: number;
104
+ };
105
+ /** Per-second histograms. */
106
+ hist: {
107
+ all: Hist;
108
+ ok: Hist;
109
+ fail: Hist;
110
+ };
111
+ apdex: {
112
+ s: number;
113
+ t: number;
114
+ f: number;
115
+ };
116
+ sys: {
117
+ /** Percent. */
118
+ cpu: number;
119
+ /** Bytes. */
120
+ memUsed: number;
121
+ memTotal: number;
122
+ memKind: 'system' | 'container';
123
+ /** Bytes; null while the disk metric is unavailable (see DECISIONS.md). */
124
+ diskUsed: number | null;
125
+ diskTotal: number | null;
126
+ load1: number | null;
127
+ };
128
+ /** Aggregated for the resources panel. */
129
+ proc: {
130
+ /** Busiest worker. */
131
+ heapPct: number;
132
+ /** Bytes, busiest worker. */
133
+ heapUsed: number;
134
+ heapLimit: number;
135
+ /** Ms, worst worker. */
136
+ lagP99: number;
137
+ };
138
+ }
139
+ interface RouteRow {
140
+ key: string;
141
+ method: string;
142
+ path: string;
143
+ count: number;
144
+ rps: number;
145
+ avg: number;
146
+ p95: number | null;
147
+ target: number;
148
+ errRate: number;
149
+ }
150
+ interface WorkerRow {
151
+ /** pm_id */
152
+ id: number;
153
+ pid: number;
154
+ status: 'online' | 'blocked' | 'stale' | 'stopped' | 'errored';
155
+ /** % of one core. */
156
+ cpu: number;
157
+ heapUsed: number;
158
+ heapLimit: number;
159
+ lagP99: number;
160
+ /** Last 10 s. */
161
+ rps: number;
162
+ /** pm2_env.restart_time */
163
+ restarts: number;
164
+ uptimeSec: number;
165
+ }
166
+ interface ErrorEntry {
167
+ t: number;
168
+ method: string;
169
+ route: string;
170
+ status: number;
171
+ message: string;
172
+ worker: number | null;
173
+ }
174
+ /** SSE `init` event and `GET basePath/api/snapshot` (spec 7.2). */
175
+ interface InitPayload {
176
+ meta: {
177
+ /** Package version. */
178
+ version: string;
179
+ /** ui.title or hostname. */
180
+ title: string;
181
+ host: string;
182
+ node: string;
183
+ platform: string;
184
+ mode: 'memory' | 'pm2';
185
+ /** 1 in memory mode. */
186
+ workers: number;
187
+ /** Epoch ms (oldest worker in PM2 mode). */
188
+ appStartedAt: number;
189
+ /** Epoch ms ("Requests since start" counts from here). */
190
+ monitorStartedAt: number;
191
+ cores: number;
192
+ };
193
+ config: {
194
+ /** BUCKETS, Infinity sent as null. */
195
+ buckets: Array<number | null>;
196
+ defaultTargetMs: number;
197
+ apdexThresholdMs: number;
198
+ thresholds: Thresholds;
199
+ windowSeconds: 900;
200
+ /** Logged-in username, shown in header menu. */
201
+ user: string;
202
+ };
203
+ totals: {
204
+ requests: number;
205
+ since: number;
206
+ };
207
+ /** Up to 900, oldest first. */
208
+ history: Tick[];
209
+ routes: RouteRow[];
210
+ /** Empty in memory mode. */
211
+ workers: WorkerRow[];
212
+ /** Newest first, up to 50. */
213
+ errors: ErrorEntry[];
214
+ }
215
+ /** SSE `tick` event, every second (spec 7.3). */
216
+ interface TickPayload {
217
+ tick: Tick;
218
+ totals: {
219
+ requests: number;
220
+ since: number;
221
+ };
222
+ /** Full table (≤ maxRoutes rows). */
223
+ routes: RouteRow[];
224
+ workers: WorkerRow[];
225
+ /** Errors since previous tick. */
226
+ newErrors: ErrorEntry[];
227
+ }
228
+
229
+ interface ServerPulseMiddleware {
230
+ (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void): void;
231
+ /** Express error middleware that records the error, then calls next(err). */
232
+ errorHandler(): (err: unknown, req: any, res: any, next: (err?: unknown) => void) => void;
233
+ /** Record an error manually (e.g. from a NestJS exception filter). */
234
+ captureError(err: unknown, req?: IncomingMessage): void;
235
+ /** Current snapshot (same shape as the SSE init payload). For tests and scripts. */
236
+ snapshot(): InitPayload;
237
+ /** Stop timers, close SSE connections, disable event loop monitoring. */
238
+ stop(): Promise<void>;
239
+ /** Resolved mode: 'memory' | 'pm2' | 'disabled'. */
240
+ readonly mode: 'memory' | 'pm2' | 'disabled';
241
+ }
242
+ declare function serverPulse(_options?: ServerPulseOptions): ServerPulseMiddleware;
243
+
244
+ export { type InitPayload, type ServerPulseMiddleware, type ServerPulseOptions, type TickPayload, serverPulse };
@@ -0,0 +1,244 @@
1
+ import { IncomingMessage, ServerResponse } from 'node:http';
2
+
3
+ /** Warning / danger thresholds used for colours and the health badge (spec 4.3). */
4
+ interface Thresholds {
5
+ /** Default [70, 85]. */
6
+ cpuPercent: [warn: number, danger: number];
7
+ /** Default [75, 90]. */
8
+ memoryPercent: [warn: number, danger: number];
9
+ /** Default [80, 90]. */
10
+ diskPercent: [warn: number, danger: number];
11
+ /** Default [70, 85] (of heap_size_limit). */
12
+ heapPercent: [warn: number, danger: number];
13
+ /** Default [50, 100] (p99 per second). */
14
+ eventLoopLagMs: [warn: number, danger: number];
15
+ /** Default [1, 3] (5xx share). */
16
+ errorRatePercent: [warn: number, danger: number];
17
+ }
18
+ interface AuthCommon {
19
+ sessionSecret?: string;
20
+ sessionTtlMinutes?: number;
21
+ secureCookie?: 'auto' | boolean;
22
+ }
23
+ interface ServerPulseOptions {
24
+ /** Turn everything off (middleware becomes a pass-through). Default: true. Env: PULSE_ENABLED=false */
25
+ enabled?: boolean;
26
+ /** URL path of the dashboard. Default: '/monitor'. Must start with '/', no trailing slash. */
27
+ basePath?: string;
28
+ /** 'auto' | 'memory' | 'pm2'. Default: 'auto' (section 11.1). Env: PULSE_MODE */
29
+ mode?: 'auto' | 'memory' | 'pm2';
30
+ /**
31
+ * Dashboard credentials. Either one user, a list of users, or false.
32
+ * Env fallbacks: PULSE_USER, PULSE_PASS, PULSE_SECRET.
33
+ * false is only allowed when NODE_ENV !== 'production'.
34
+ */
35
+ auth?: ({
36
+ username: string;
37
+ password: string;
38
+ } & AuthCommon) | ({
39
+ users: Array<{
40
+ username: string;
41
+ password: string;
42
+ }>;
43
+ } & AuthCommon) | false;
44
+ /** Only these client IPs may reach the dashboard (exact IPv4/IPv6 or IPv4 CIDR). Others get 404. Default: allow all. */
45
+ ipAllowList?: string[];
46
+ /** Trust X-Forwarded-For / X-Forwarded-Proto (set true behind Nginx or a load balancer). Default: false. */
47
+ trustProxy?: boolean;
48
+ /** Extra requests to exclude from statistics. basePath is always excluded. */
49
+ ignore?: Array<string | RegExp | ((req: IncomingMessage) => boolean)>;
50
+ /** Apply the default ignore list (/health, /favicon.ico, static files). Default: true. */
51
+ ignoreDefaults?: boolean;
52
+ routes?: {
53
+ /** Per-route response-time targets in ms, keyed "METHOD /normalized/path". */
54
+ targets?: Record<string, number>;
55
+ /** Target for routes not listed above. Default: 500. */
56
+ defaultTargetMs?: number;
57
+ /** Maximum distinct routes tracked; extra routes go to "OTHER". Default: 500. */
58
+ maxRoutes?: number;
59
+ /** Custom normalizer; return null to fall back to the built-in one. */
60
+ normalize?: (req: IncomingMessage) => string | null;
61
+ };
62
+ /** Apdex threshold T in ms. Default: 300. */
63
+ apdexThresholdMs?: number;
64
+ /** Warning / danger thresholds used for colours and the health badge (section 4.3). */
65
+ thresholds?: Partial<Thresholds>;
66
+ /** Path used for the disk metric. Default: '/' (Windows: drive of process.cwd()). */
67
+ diskPath?: string;
68
+ pm2?: {
69
+ /** Monitor process host and port. Defaults: '127.0.0.1', 9100. Env: PULSE_MONITOR_HOST, PULSE_MONITOR_PORT */
70
+ monitorHost?: string;
71
+ monitorPort?: number;
72
+ };
73
+ ui?: {
74
+ /** Title shown in the header. Default: os.hostname(). */
75
+ title?: string;
76
+ /** Load IBM Plex Sans from Google Fonts. Default: false (system font stack, no external requests). */
77
+ webFonts?: boolean;
78
+ };
79
+ /** Optional health endpoint, outside basePath, no auth. Default: false. */
80
+ health?: false | {
81
+ path?: string;
82
+ checks?: Record<string, () => boolean | Promise<boolean>>;
83
+ };
84
+ /** Logger. Default: console with "[server-pulse]" prefix. */
85
+ logger?: Pick<Console, 'info' | 'warn' | 'error'>;
86
+ }
87
+
88
+ /** Histogram counts, length 18, in BUCKETS order. */
89
+ type Hist = number[];
90
+ /** One second of aggregated data (spec 7.1). */
91
+ interface Tick {
92
+ /** Epoch ms, start of the second. */
93
+ t: number;
94
+ req: {
95
+ /** Finished requests in this second. */
96
+ total: number;
97
+ c2: number;
98
+ c3: number;
99
+ c4: number;
100
+ c5: number;
101
+ aborted: number;
102
+ /** Value at the end of the second. */
103
+ inFlight: number;
104
+ };
105
+ /** Per-second histograms. */
106
+ hist: {
107
+ all: Hist;
108
+ ok: Hist;
109
+ fail: Hist;
110
+ };
111
+ apdex: {
112
+ s: number;
113
+ t: number;
114
+ f: number;
115
+ };
116
+ sys: {
117
+ /** Percent. */
118
+ cpu: number;
119
+ /** Bytes. */
120
+ memUsed: number;
121
+ memTotal: number;
122
+ memKind: 'system' | 'container';
123
+ /** Bytes; null while the disk metric is unavailable (see DECISIONS.md). */
124
+ diskUsed: number | null;
125
+ diskTotal: number | null;
126
+ load1: number | null;
127
+ };
128
+ /** Aggregated for the resources panel. */
129
+ proc: {
130
+ /** Busiest worker. */
131
+ heapPct: number;
132
+ /** Bytes, busiest worker. */
133
+ heapUsed: number;
134
+ heapLimit: number;
135
+ /** Ms, worst worker. */
136
+ lagP99: number;
137
+ };
138
+ }
139
+ interface RouteRow {
140
+ key: string;
141
+ method: string;
142
+ path: string;
143
+ count: number;
144
+ rps: number;
145
+ avg: number;
146
+ p95: number | null;
147
+ target: number;
148
+ errRate: number;
149
+ }
150
+ interface WorkerRow {
151
+ /** pm_id */
152
+ id: number;
153
+ pid: number;
154
+ status: 'online' | 'blocked' | 'stale' | 'stopped' | 'errored';
155
+ /** % of one core. */
156
+ cpu: number;
157
+ heapUsed: number;
158
+ heapLimit: number;
159
+ lagP99: number;
160
+ /** Last 10 s. */
161
+ rps: number;
162
+ /** pm2_env.restart_time */
163
+ restarts: number;
164
+ uptimeSec: number;
165
+ }
166
+ interface ErrorEntry {
167
+ t: number;
168
+ method: string;
169
+ route: string;
170
+ status: number;
171
+ message: string;
172
+ worker: number | null;
173
+ }
174
+ /** SSE `init` event and `GET basePath/api/snapshot` (spec 7.2). */
175
+ interface InitPayload {
176
+ meta: {
177
+ /** Package version. */
178
+ version: string;
179
+ /** ui.title or hostname. */
180
+ title: string;
181
+ host: string;
182
+ node: string;
183
+ platform: string;
184
+ mode: 'memory' | 'pm2';
185
+ /** 1 in memory mode. */
186
+ workers: number;
187
+ /** Epoch ms (oldest worker in PM2 mode). */
188
+ appStartedAt: number;
189
+ /** Epoch ms ("Requests since start" counts from here). */
190
+ monitorStartedAt: number;
191
+ cores: number;
192
+ };
193
+ config: {
194
+ /** BUCKETS, Infinity sent as null. */
195
+ buckets: Array<number | null>;
196
+ defaultTargetMs: number;
197
+ apdexThresholdMs: number;
198
+ thresholds: Thresholds;
199
+ windowSeconds: 900;
200
+ /** Logged-in username, shown in header menu. */
201
+ user: string;
202
+ };
203
+ totals: {
204
+ requests: number;
205
+ since: number;
206
+ };
207
+ /** Up to 900, oldest first. */
208
+ history: Tick[];
209
+ routes: RouteRow[];
210
+ /** Empty in memory mode. */
211
+ workers: WorkerRow[];
212
+ /** Newest first, up to 50. */
213
+ errors: ErrorEntry[];
214
+ }
215
+ /** SSE `tick` event, every second (spec 7.3). */
216
+ interface TickPayload {
217
+ tick: Tick;
218
+ totals: {
219
+ requests: number;
220
+ since: number;
221
+ };
222
+ /** Full table (≤ maxRoutes rows). */
223
+ routes: RouteRow[];
224
+ workers: WorkerRow[];
225
+ /** Errors since previous tick. */
226
+ newErrors: ErrorEntry[];
227
+ }
228
+
229
+ interface ServerPulseMiddleware {
230
+ (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void): void;
231
+ /** Express error middleware that records the error, then calls next(err). */
232
+ errorHandler(): (err: unknown, req: any, res: any, next: (err?: unknown) => void) => void;
233
+ /** Record an error manually (e.g. from a NestJS exception filter). */
234
+ captureError(err: unknown, req?: IncomingMessage): void;
235
+ /** Current snapshot (same shape as the SSE init payload). For tests and scripts. */
236
+ snapshot(): InitPayload;
237
+ /** Stop timers, close SSE connections, disable event loop monitoring. */
238
+ stop(): Promise<void>;
239
+ /** Resolved mode: 'memory' | 'pm2' | 'disabled'. */
240
+ readonly mode: 'memory' | 'pm2' | 'disabled';
241
+ }
242
+ declare function serverPulse(_options?: ServerPulseOptions): ServerPulseMiddleware;
243
+
244
+ export { type InitPayload, type ServerPulseMiddleware, type ServerPulseOptions, type TickPayload, serverPulse };
package/dist/index.mjs ADDED
@@ -0,0 +1,60 @@
1
+ // src/index.ts
2
+ import os from "os";
3
+
4
+ // package.json
5
+ var version = "0.0.1";
6
+
7
+ // src/index.ts
8
+ var DEFAULT_THRESHOLDS = {
9
+ cpuPercent: [70, 85],
10
+ memoryPercent: [75, 90],
11
+ diskPercent: [80, 90],
12
+ heapPercent: [70, 85],
13
+ eventLoopLagMs: [50, 100],
14
+ errorRatePercent: [1, 3]
15
+ };
16
+ var BUCKETS_WIRE = [5, 10, 25, 50, 75, 100, 150, 250, 350, 500, 750, 1e3, 1500, 2500, 5e3, 1e4, 3e4, null];
17
+ function serverPulse(_options) {
18
+ const startedAt = Date.now();
19
+ const middleware = ((_req, _res, next) => {
20
+ next();
21
+ });
22
+ middleware.errorHandler = () => (err, _req, _res, next) => next(err);
23
+ middleware.captureError = () => {
24
+ };
25
+ middleware.snapshot = () => ({
26
+ meta: {
27
+ version,
28
+ title: os.hostname(),
29
+ host: os.hostname(),
30
+ node: process.version,
31
+ platform: process.platform,
32
+ mode: "memory",
33
+ workers: 1,
34
+ appStartedAt: Math.round(Date.now() - process.uptime() * 1e3),
35
+ monitorStartedAt: startedAt,
36
+ cores: os.availableParallelism?.() ?? os.cpus().length
37
+ },
38
+ config: {
39
+ buckets: [...BUCKETS_WIRE],
40
+ defaultTargetMs: 500,
41
+ apdexThresholdMs: 300,
42
+ thresholds: structuredClone(DEFAULT_THRESHOLDS),
43
+ windowSeconds: 900,
44
+ user: ""
45
+ },
46
+ totals: { requests: 0, since: startedAt },
47
+ history: [],
48
+ routes: [],
49
+ workers: [],
50
+ errors: []
51
+ });
52
+ middleware.stop = async () => {
53
+ };
54
+ Object.defineProperty(middleware, "mode", { value: "disabled", enumerable: true });
55
+ return middleware;
56
+ }
57
+ export {
58
+ serverPulse
59
+ };
60
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../package.json"],"sourcesContent":["import type { IncomingMessage, ServerResponse } from 'node:http';\nimport os from 'node:os';\nimport type { ServerPulseOptions, Thresholds } from './options';\nimport type { InitPayload, TickPayload } from './engine/payloads';\nimport { version } from '../package.json';\n\nexport interface ServerPulseMiddleware {\n (req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => void): void;\n /** Express error middleware that records the error, then calls next(err). */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n errorHandler(): (err: unknown, req: any, res: any, next: (err?: unknown) => void) => void;\n /** Record an error manually (e.g. from a NestJS exception filter). */\n captureError(err: unknown, req?: IncomingMessage): void;\n /** Current snapshot (same shape as the SSE init payload). For tests and scripts. */\n snapshot(): InitPayload;\n /** Stop timers, close SSE connections, disable event loop monitoring. */\n stop(): Promise<void>;\n /** Resolved mode: 'memory' | 'pm2' | 'disabled'. */\n readonly mode: 'memory' | 'pm2' | 'disabled';\n}\n\n// M0 scaffold: pass-through middleware. Collection, dashboard and PM2 mode arrive in later milestones.\nconst DEFAULT_THRESHOLDS: Thresholds = {\n cpuPercent: [70, 85],\n memoryPercent: [75, 90],\n diskPercent: [80, 90],\n heapPercent: [70, 85],\n eventLoopLagMs: [50, 100],\n errorRatePercent: [1, 3],\n};\n\nconst BUCKETS_WIRE = [5, 10, 25, 50, 75, 100, 150, 250, 350, 500, 750, 1000, 1500, 2500, 5000, 10000, 30000, null];\n\nexport function serverPulse(_options?: ServerPulseOptions): ServerPulseMiddleware {\n const startedAt = Date.now();\n\n const middleware = ((_req: IncomingMessage, _res: ServerResponse, next: (err?: unknown) => void) => {\n next();\n }) as ServerPulseMiddleware;\n\n middleware.errorHandler = () => (err, _req, _res, next) => next(err);\n middleware.captureError = () => {};\n middleware.snapshot = () => ({\n meta: {\n version,\n title: os.hostname(),\n host: os.hostname(),\n node: process.version,\n platform: process.platform,\n mode: 'memory',\n workers: 1,\n appStartedAt: Math.round(Date.now() - process.uptime() * 1000),\n monitorStartedAt: startedAt,\n cores: os.availableParallelism?.() ?? os.cpus().length,\n },\n config: {\n buckets: [...BUCKETS_WIRE],\n defaultTargetMs: 500,\n apdexThresholdMs: 300,\n thresholds: structuredClone(DEFAULT_THRESHOLDS),\n windowSeconds: 900,\n user: '',\n },\n totals: { requests: 0, since: startedAt },\n history: [],\n routes: [],\n workers: [],\n errors: [],\n });\n middleware.stop = async () => {};\n Object.defineProperty(middleware, 'mode', { value: 'disabled', enumerable: true });\n\n return middleware;\n}\n\nexport type { ServerPulseOptions, InitPayload, TickPayload };\n","{\n \"name\": \"@pulse-labs/server-pulse\",\n \"version\": \"0.0.1\",\n \"license\": \"MIT\",\n \"description\": \"Live server monitoring dashboard for Node.js apps, served from your own base URL.\",\n \"engines\": {\n \"node\": \">=18.15\"\n },\n \"type\": \"commonjs\",\n \"main\": \"./dist/index.cjs\",\n \"module\": \"./dist/index.mjs\",\n \"types\": \"./dist/index.d.ts\",\n \"exports\": {\n \".\": {\n \"import\": {\n \"types\": \"./dist/index.d.mts\",\n \"default\": \"./dist/index.mjs\"\n },\n \"require\": {\n \"types\": \"./dist/index.d.ts\",\n \"default\": \"./dist/index.cjs\"\n }\n },\n \"./package.json\": \"./package.json\"\n },\n \"bin\": {\n \"server-pulse\": \"dist/cli.cjs\"\n },\n \"files\": [\n \"dist\",\n \"README.md\",\n \"CHANGELOG.md\"\n ],\n \"publishConfig\": {\n \"access\": \"public\"\n },\n \"scripts\": {\n \"build\": \"tsup && node scripts/copy-ui.mjs\",\n \"prepare\": \"npm run build\",\n \"prepublishOnly\": \"npm run lint && npm run typecheck && npm test\",\n \"dev\": \"tsup --watch\",\n \"lint\": \"eslint .\",\n \"format\": \"prettier --write .\",\n \"typecheck\": \"tsc --noEmit\",\n \"test\": \"vitest run\",\n \"test:watch\": \"vitest\",\n \"smoke\": \"node scripts/smoke-dist.mjs\",\n \"test:pm2\": \"vitest run test/integration/pm2.test.ts\",\n \"test:ui\": \"node test/ui/run.mjs\",\n \"bench\": \"node bench/overhead.mjs\"\n },\n \"peerDependencies\": {\n \"pm2\": \">=5\"\n },\n \"peerDependenciesMeta\": {\n \"pm2\": {\n \"optional\": true\n }\n },\n \"devDependencies\": {\n \"@eslint/js\": \"^9.39.5\",\n \"@types/express\": \"^5.0.6\",\n \"@types/node\": \"^20.19.43\",\n \"@types/supertest\": \"^7.2.1\",\n \"@vitest/coverage-v8\": \"^4.1.11\",\n \"eslint\": \"^9.39.5\",\n \"express4\": \"npm:express@^4.22.3\",\n \"express5\": \"npm:express@^5.3.0\",\n \"globals\": \"^17.13.0\",\n \"prettier\": \"^3.9.10\",\n \"supertest\": \"^7.3.1\",\n \"tsup\": \"^8.5.1\",\n \"typescript\": \"~5.9.3\",\n \"typescript-eslint\": \"^8.71.2\",\n \"vite\": \"^6.4.4\",\n \"vitest\": \"^4.1.11\"\n },\n \"overrides\": {\n \"tsup\": {\n \"esbuild\": \"^0.28.2\",\n \"bundle-require\": {\n \"esbuild\": \"^0.28.2\"\n }\n }\n },\n \"keywords\": [\n \"monitoring\",\n \"dashboard\",\n \"express\",\n \"nestjs\",\n \"pm2\",\n \"metrics\",\n \"sse\"\n ]\n}\n"],"mappings":";AACA,OAAO,QAAQ;;;ACCb,cAAW;;;ADoBb,IAAM,qBAAiC;AAAA,EACrC,YAAY,CAAC,IAAI,EAAE;AAAA,EACnB,eAAe,CAAC,IAAI,EAAE;AAAA,EACtB,aAAa,CAAC,IAAI,EAAE;AAAA,EACpB,aAAa,CAAC,IAAI,EAAE;AAAA,EACpB,gBAAgB,CAAC,IAAI,GAAG;AAAA,EACxB,kBAAkB,CAAC,GAAG,CAAC;AACzB;AAEA,IAAM,eAAe,CAAC,GAAG,IAAI,IAAI,IAAI,IAAI,KAAK,KAAK,KAAK,KAAK,KAAK,KAAK,KAAM,MAAM,MAAM,KAAM,KAAO,KAAO,IAAI;AAE1G,SAAS,YAAY,UAAsD;AAChF,QAAM,YAAY,KAAK,IAAI;AAE3B,QAAM,cAAc,CAAC,MAAuB,MAAsB,SAAkC;AAClG,SAAK;AAAA,EACP;AAEA,aAAW,eAAe,MAAM,CAAC,KAAK,MAAM,MAAM,SAAS,KAAK,GAAG;AACnE,aAAW,eAAe,MAAM;AAAA,EAAC;AACjC,aAAW,WAAW,OAAO;AAAA,IAC3B,MAAM;AAAA,MACJ;AAAA,MACA,OAAO,GAAG,SAAS;AAAA,MACnB,MAAM,GAAG,SAAS;AAAA,MAClB,MAAM,QAAQ;AAAA,MACd,UAAU,QAAQ;AAAA,MAClB,MAAM;AAAA,MACN,SAAS;AAAA,MACT,cAAc,KAAK,MAAM,KAAK,IAAI,IAAI,QAAQ,OAAO,IAAI,GAAI;AAAA,MAC7D,kBAAkB;AAAA,MAClB,OAAO,GAAG,uBAAuB,KAAK,GAAG,KAAK,EAAE;AAAA,IAClD;AAAA,IACA,QAAQ;AAAA,MACN,SAAS,CAAC,GAAG,YAAY;AAAA,MACzB,iBAAiB;AAAA,MACjB,kBAAkB;AAAA,MAClB,YAAY,gBAAgB,kBAAkB;AAAA,MAC9C,eAAe;AAAA,MACf,MAAM;AAAA,IACR;AAAA,IACA,QAAQ,EAAE,UAAU,GAAG,OAAO,UAAU;AAAA,IACxC,SAAS,CAAC;AAAA,IACV,QAAQ,CAAC;AAAA,IACT,SAAS,CAAC;AAAA,IACV,QAAQ,CAAC;AAAA,EACX;AACA,aAAW,OAAO,YAAY;AAAA,EAAC;AAC/B,SAAO,eAAe,YAAY,QAAQ,EAAE,OAAO,YAAY,YAAY,KAAK,CAAC;AAEjF,SAAO;AACT;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,95 @@
1
1
  {
2
2
  "name": "@pulse-labs/server-pulse",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "description": "Live server monitoring dashboard for Node.js apps, served from your own base URL.",
6
+ "engines": {
7
+ "node": ">=18.15"
8
+ },
9
+ "type": "commonjs",
10
+ "main": "./dist/index.cjs",
11
+ "module": "./dist/index.mjs",
12
+ "types": "./dist/index.d.ts",
13
+ "exports": {
14
+ ".": {
15
+ "import": {
16
+ "types": "./dist/index.d.mts",
17
+ "default": "./dist/index.mjs"
18
+ },
19
+ "require": {
20
+ "types": "./dist/index.d.ts",
21
+ "default": "./dist/index.cjs"
22
+ }
23
+ },
24
+ "./package.json": "./package.json"
25
+ },
26
+ "bin": {
27
+ "server-pulse": "dist/cli.cjs"
28
+ },
29
+ "files": [
30
+ "dist",
31
+ "README.md",
32
+ "CHANGELOG.md"
33
+ ],
34
+ "publishConfig": {
35
+ "access": "public"
36
+ },
37
+ "scripts": {
38
+ "build": "tsup && node scripts/copy-ui.mjs",
39
+ "prepare": "npm run build",
40
+ "prepublishOnly": "npm run lint && npm run typecheck && npm test",
41
+ "dev": "tsup --watch",
42
+ "lint": "eslint .",
43
+ "format": "prettier --write .",
44
+ "typecheck": "tsc --noEmit",
45
+ "test": "vitest run",
46
+ "test:watch": "vitest",
47
+ "smoke": "node scripts/smoke-dist.mjs",
48
+ "test:pm2": "vitest run test/integration/pm2.test.ts",
49
+ "test:ui": "node test/ui/run.mjs",
50
+ "bench": "node bench/overhead.mjs"
51
+ },
52
+ "peerDependencies": {
53
+ "pm2": ">=5"
54
+ },
55
+ "peerDependenciesMeta": {
56
+ "pm2": {
57
+ "optional": true
58
+ }
59
+ },
60
+ "devDependencies": {
61
+ "@eslint/js": "^9.39.5",
62
+ "@types/express": "^5.0.6",
63
+ "@types/node": "^20.19.43",
64
+ "@types/supertest": "^7.2.1",
65
+ "@vitest/coverage-v8": "^4.1.11",
66
+ "eslint": "^9.39.5",
67
+ "express4": "npm:express@^4.22.3",
68
+ "express5": "npm:express@^5.3.0",
69
+ "globals": "^17.13.0",
70
+ "prettier": "^3.9.10",
71
+ "supertest": "^7.3.1",
72
+ "tsup": "^8.5.1",
73
+ "typescript": "~5.9.3",
74
+ "typescript-eslint": "^8.71.2",
75
+ "vite": "^6.4.4",
76
+ "vitest": "^4.1.11"
77
+ },
78
+ "overrides": {
79
+ "tsup": {
80
+ "esbuild": "^0.28.2",
81
+ "bundle-require": {
82
+ "esbuild": "^0.28.2"
83
+ }
84
+ }
85
+ },
86
+ "keywords": [
87
+ "monitoring",
88
+ "dashboard",
89
+ "express",
90
+ "nestjs",
91
+ "pm2",
92
+ "metrics",
93
+ "sse"
94
+ ]
95
+ }