diff --git a/Dockerfile b/Dockerfile index ecb1766..4094a66 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,29 @@ -FROM node:lts-alpine - -WORKDIR /app +FROM node:lts-alpine AS build RUN apk add --no-cache openssl +WORKDIR /app + # Copy only package files and install deps # This layer will be cached as long as package*.json don't change COPY package*.json package-lock.json* ./ -RUN npm ci +RUN --mount=type=cache,target=/root/.npm npm ci --omit=dev -# Copy the rest of the source +# Copy the rest of your source COPY . . -EXPOSE 8080 \ No newline at end of file +# Pre-generate prisma client +RUN node_modules/.bin/prisma generate + +FROM node:lts-alpine + +RUN apk add --no-cache openssl + +USER node:node + +WORKDIR /app + +COPY --from=build --chown=node:node /app . + + +EXPOSE 8080 diff --git a/README.md b/README.md index 854146a..2ee653b 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,11 @@ git clone https://github.com/liamcottle/meshtastic-map cd meshtastic-map ``` +Install Meshtastic protobufs definitions +``` +git clone https://github.com/meshtastic/protobufs src/protobufs +``` + Install NodeJS dependencies ``` diff --git a/docker-compose.yml b/docker-compose.yml index 85d17e6..026bb0c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,6 +30,18 @@ services: DATABASE_URL: "mysql://root:password@database:3306/meshtastic-map?connection_limit=100" MAP_OPTS: "" # add any custom index.js options here + # publishes mqtt packets via websocket + meshtastic-ws: + container_name: meshtastic-ws + build: + context: . + dockerfile: ./Dockerfile + command: /app/docker/ws.sh + ports: + - 8081:8081/tcp + environment: + WS_OPTS: "" + # runs the database to store everything from mqtt database: container_name: database diff --git a/docker/ws.sh b/docker/ws.sh new file mode 100755 index 0000000..71d4048 --- /dev/null +++ b/docker/ws.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +echo "Starting websocket publisher" +exec node src/ws.js ${WS_OPTS} + diff --git a/package-lock.json b/package-lock.json index 698e702..1fb2318 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,15 +14,30 @@ "command-line-usage": "^7.0.3", "compression": "^1.8.1", "cors": "^2.8.5", - "express": "^5.0.0", + "express": "^5.2.1", "mqtt": "^5.14.1", - "protobufjs": "^7.5.4" + "protobufjs": "^7.5.4", + "ws": "^8.18.3" }, "devDependencies": { - "jest": "^30.2.0", + "jest": "^30.1.3", "prisma": "^6.16.2" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -39,9 +54,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", - "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", "dev": true, "license": "MIT", "engines": { @@ -49,22 +64,23 @@ } }, "node_modules/@babel/core": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", - "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { + "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.4", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", - "@jridgewell/remapping": "^2.3.5", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.2", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -80,9 +96,9 @@ } }, "node_modules/@babel/core/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -231,27 +247,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" + "@babel/types": "^7.28.2" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.4" + "@babel/types": "^7.28.2" }, "bin": { "parser": "bin/babel-parser.js" @@ -524,18 +540,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.3", "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", + "@babel/parser": "^7.28.3", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", + "@babel/types": "^7.28.2", "debug": "^4.3.1" }, "engines": { @@ -543,9 +559,9 @@ } }, "node_modules/@babel/traverse/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -568,9 +584,9 @@ "license": "MIT" }, "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", "dev": true, "license": "MIT", "dependencies": { @@ -668,17 +684,17 @@ } }, "node_modules/@jest/console": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", - "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.1.2.tgz", + "integrity": "sha512-BGMAxj8VRmoD0MoA/jo9alMXSRoqW8KPeqOfEo1ncxnRLatTBCpRoOwlwlEMdudp68Q6WSGwYrrLtTGOh8fLzw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", - "jest-message-util": "30.2.0", - "jest-util": "30.2.0", + "jest-message-util": "30.1.0", + "jest-util": "30.0.5", "slash": "^3.0.0" }, "engines": { @@ -686,39 +702,39 @@ } }, "node_modules/@jest/core": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", - "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.1.3.tgz", + "integrity": "sha512-LIQz7NEDDO1+eyOA2ZmkiAyYvZuo6s1UxD/e2IHldR6D7UYogVq3arTmli07MkENLq6/3JEQjp0mA8rrHHJ8KQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.2.0", + "@jest/console": "30.1.2", "@jest/pattern": "30.0.1", - "@jest/reporters": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", + "@jest/reporters": "30.1.3", + "@jest/test-result": "30.1.3", + "@jest/transform": "30.1.2", + "@jest/types": "30.0.5", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "ci-info": "^4.2.0", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", - "jest-changed-files": "30.2.0", - "jest-config": "30.2.0", - "jest-haste-map": "30.2.0", - "jest-message-util": "30.2.0", + "jest-changed-files": "30.0.5", + "jest-config": "30.1.3", + "jest-haste-map": "30.1.0", + "jest-message-util": "30.1.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.2.0", - "jest-resolve-dependencies": "30.2.0", - "jest-runner": "30.2.0", - "jest-runtime": "30.2.0", - "jest-snapshot": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", - "jest-watcher": "30.2.0", + "jest-resolve": "30.1.3", + "jest-resolve-dependencies": "30.1.3", + "jest-runner": "30.1.3", + "jest-runtime": "30.1.3", + "jest-snapshot": "30.1.2", + "jest-util": "30.0.5", + "jest-validate": "30.1.0", + "jest-watcher": "30.1.3", "micromatch": "^4.0.8", - "pretty-format": "30.2.0", + "pretty-format": "30.0.5", "slash": "^3.0.0" }, "engines": { @@ -744,39 +760,39 @@ } }, "node_modules/@jest/environment": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", - "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.1.2.tgz", + "integrity": "sha512-N8t1Ytw4/mr9uN28OnVf0SYE2dGhaIxOVYcwsf9IInBKjvofAjbFRvedvBBlyTYk2knbJTiEjEJ2PyyDIBnd9w==", "dev": true, "license": "MIT", "dependencies": { - "@jest/fake-timers": "30.2.0", - "@jest/types": "30.2.0", + "@jest/fake-timers": "30.1.2", + "@jest/types": "30.0.5", "@types/node": "*", - "jest-mock": "30.2.0" + "jest-mock": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", - "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.1.2.tgz", + "integrity": "sha512-tyaIExOwQRCxPCGNC05lIjWJztDwk2gPDNSDGg1zitXJJ8dC3++G/CRjE5mb2wQsf89+lsgAgqxxNpDLiCViTA==", "dev": true, "license": "MIT", "dependencies": { - "expect": "30.2.0", - "jest-snapshot": "30.2.0" + "expect": "30.1.2", + "jest-snapshot": "30.1.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", - "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.1.2.tgz", + "integrity": "sha512-HXy1qT/bfdjCv7iC336ExbqqYtZvljrV8odNdso7dWK9bSeHtLlvwWWC3YSybSPL03Gg5rug6WLCZAZFH72m0A==", "dev": true, "license": "MIT", "dependencies": { @@ -787,18 +803,18 @@ } }, "node_modules/@jest/fake-timers": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", - "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.1.2.tgz", + "integrity": "sha512-Beljfv9AYkr9K+ETX9tvV61rJTY706BhBUtiaepQHeEGfe0DbpvUA5Z3fomwc5Xkhns6NWrcFDZn+72fLieUnA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.0.5", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" + "jest-message-util": "30.1.0", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -815,16 +831,16 @@ } }, "node_modules/@jest/globals": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", - "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.1.2.tgz", + "integrity": "sha512-teNTPZ8yZe3ahbYnvnVRDeOjr+3pu2uiAtNtrEsiMjVPPj+cXd5E/fr8BL7v/T7F31vYdEHrI5cC/2OoO/vM9A==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.2.0", - "@jest/expect": "30.2.0", - "@jest/types": "30.2.0", - "jest-mock": "30.2.0" + "@jest/environment": "30.1.2", + "@jest/expect": "30.1.2", + "@jest/types": "30.0.5", + "jest-mock": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -845,17 +861,17 @@ } }, "node_modules/@jest/reporters": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", - "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.1.3.tgz", + "integrity": "sha512-VWEQmJWfXMOrzdFEOyGjUEOuVXllgZsoPtEHZzfdNz18RmzJ5nlR6kp8hDdY8dDS1yGOXAY7DHT+AOHIPSBV0w==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", + "@jest/console": "30.1.2", + "@jest/test-result": "30.1.3", + "@jest/transform": "30.1.2", + "@jest/types": "30.0.5", "@jridgewell/trace-mapping": "^0.3.25", "@types/node": "*", "chalk": "^4.1.2", @@ -868,9 +884,9 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^5.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "30.2.0", - "jest-util": "30.2.0", - "jest-worker": "30.2.0", + "jest-message-util": "30.1.0", + "jest-util": "30.0.5", + "jest-worker": "30.1.0", "slash": "^3.0.0", "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" @@ -901,13 +917,13 @@ } }, "node_modules/@jest/snapshot-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", - "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.1.2.tgz", + "integrity": "sha512-vHoMTpimcPSR7OxS2S0V1Cpg8eKDRxucHjoWl5u4RQcnxqQrV3avETiFpl8etn4dqxEGarBeHbIBety/f8mLXw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.0.5", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "natural-compare": "^1.4.0" @@ -932,14 +948,14 @@ } }, "node_modules/@jest/test-result": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", - "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.1.3.tgz", + "integrity": "sha512-P9IV8T24D43cNRANPPokn7tZh0FAFnYS2HIfi5vK18CjRkTDR9Y3e1BoEcAJnl4ghZZF4Ecda4M/k41QkvurEQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.2.0", - "@jest/types": "30.2.0", + "@jest/console": "30.1.2", + "@jest/types": "30.0.5", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" }, @@ -948,15 +964,15 @@ } }, "node_modules/@jest/test-sequencer": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", - "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.1.3.tgz", + "integrity": "sha512-82J+hzC0qeQIiiZDThh+YUadvshdBswi5nuyXlEmXzrhw5ZQSRHeQ5LpVMD/xc8B3wPePvs6VMzHnntxL+4E3w==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.2.0", + "@jest/test-result": "30.1.3", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", + "jest-haste-map": "30.1.0", "slash": "^3.0.0" }, "engines": { @@ -964,23 +980,23 @@ } }, "node_modules/@jest/transform": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", - "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.1.2.tgz", + "integrity": "sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", - "@jest/types": "30.2.0", + "@jest/types": "30.0.5", "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.1", + "babel-plugin-istanbul": "^7.0.0", "chalk": "^4.1.2", "convert-source-map": "^2.0.0", "fast-json-stable-stringify": "^2.1.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", + "jest-haste-map": "30.1.0", "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", + "jest-util": "30.0.5", "micromatch": "^4.0.8", "pirates": "^4.0.7", "slash": "^3.0.0", @@ -991,9 +1007,9 @@ } }, "node_modules/@jest/types": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", - "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1020,17 +1036,6 @@ "@jridgewell/trace-mapping": "^0.3.24" } }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -1049,9 +1054,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", "dev": true, "license": "MIT", "dependencies": { @@ -1270,9 +1275,9 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", + "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", "dev": true, "license": "MIT", "optional": true, @@ -1707,9 +1712,9 @@ } }, "node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", "dev": true, "license": "MIT", "engines": { @@ -1767,16 +1772,16 @@ } }, "node_modules/babel-jest": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", - "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.1.2.tgz", + "integrity": "sha512-IQCus1rt9kaSh7PQxLYRY5NmkNrNlU2TpabzwV7T2jljnpdHOcmnYYv8QmE04Li4S3a2Lj8/yXyET5pBarPr6g==", "dev": true, "license": "MIT", "dependencies": { - "@jest/transform": "30.2.0", + "@jest/transform": "30.1.2", "@types/babel__core": "^7.20.5", - "babel-plugin-istanbul": "^7.0.1", - "babel-preset-jest": "30.2.0", + "babel-plugin-istanbul": "^7.0.0", + "babel-preset-jest": "30.0.1", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "slash": "^3.0.0" @@ -1785,18 +1790,15 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.11.0 || ^8.0.0-0" + "@babel/core": "^7.11.0" } }, "node_modules/babel-plugin-istanbul": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", - "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", + "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", "dev": true, "license": "BSD-3-Clause", - "workspaces": [ - "test/babel-8" - ], "dependencies": { "@babel/helper-plugin-utils": "^7.0.0", "@istanbuljs/load-nyc-config": "^1.0.0", @@ -1809,12 +1811,14 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", - "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz", + "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==", "dev": true, "license": "MIT", "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", "@types/babel__core": "^7.20.5" }, "engines": { @@ -1849,20 +1853,20 @@ } }, "node_modules/babel-preset-jest": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", - "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", + "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", "dev": true, "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "30.2.0", - "babel-preset-current-node-syntax": "^1.2.0" + "babel-plugin-jest-hoist": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + "@babel/core": "^7.11.0" } }, "node_modules/balanced-match": { @@ -1892,16 +1896,6 @@ ], "license": "MIT" }, - "node_modules/baseline-browser-mapping": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.9.tgz", - "integrity": "sha512-hY/u2lxLrbecMEWSB0IpGzGyDyeoMFQhCvZd2jGFSE5I17Fh01sYUBPCJtkWERw7zrac9+cIghxm/ytJa2X8iA==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, "node_modules/bl": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-6.1.0.tgz", @@ -1915,28 +1909,34 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", + "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/body-parser/node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -1952,7 +1952,8 @@ "node_modules/body-parser/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" }, "node_modules/brace-expansion": { "version": "2.0.2", @@ -1990,9 +1991,9 @@ } }, "node_modules/browserslist": { - "version": "4.26.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", - "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", + "version": "4.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", "dev": true, "funding": [ { @@ -2009,11 +2010,11 @@ } ], "license": "MIT", + "peer": true, "dependencies": { - "baseline-browser-mapping": "^2.8.3", - "caniuse-lite": "^1.0.30001741", - "electron-to-chromium": "^1.5.218", - "node-releases": "^2.0.21", + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", + "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { @@ -2104,6 +2105,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" @@ -2116,6 +2118,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" @@ -2148,9 +2151,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001745", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", - "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", + "version": "1.0.30001739", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", + "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", "dev": true, "funding": [ { @@ -2509,6 +2512,7 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "license": "MIT", "engines": { "node": ">= 0.6" } @@ -2572,9 +2576,9 @@ } }, "node_modules/dedent": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", - "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2655,6 +2659,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", @@ -2688,9 +2693,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.227", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.227.tgz", - "integrity": "sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==", + "version": "1.5.212", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.212.tgz", + "integrity": "sha512-gE7ErIzSW+d8jALWMcOIgf+IB6lpfsg6NwOhPVwKzDtN2qcBix47vlin4yzSregYDxTCXOUqAZjVY/Z3naS7ww==", "dev": true, "license": "ISC" }, @@ -2733,9 +2738,9 @@ } }, "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", "dev": true, "license": "MIT", "dependencies": { @@ -2746,6 +2751,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -2754,6 +2760,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -2762,6 +2769,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0" }, @@ -2876,35 +2884,37 @@ } }, "node_modules/expect": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", - "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.1.2.tgz", + "integrity": "sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.2.0", + "@jest/expect-utils": "30.1.2", "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.2.0", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", - "jest-util": "30.2.0" + "jest-matcher-utils": "30.1.2", + "jest-message-util": "30.1.0", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/express": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", - "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", + "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", + "license": "MIT", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.2.0", + "body-parser": "^2.2.1", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", + "depd": "^2.0.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", @@ -2962,25 +2972,6 @@ } } }, - "node_modules/express/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/express/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -3211,6 +3202,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -3239,6 +3231,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", @@ -3272,6 +3265,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" @@ -3336,6 +3330,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3362,6 +3357,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -3373,6 +3369,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", "dependencies": { "function-bind": "^1.1.2" }, @@ -3394,26 +3391,23 @@ "license": "MIT" }, "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "license": "MIT", "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" }, "engines": { "node": ">= 0.8" - } - }, - "node_modules/http-errors/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/human-signals": { @@ -3427,14 +3421,19 @@ } }, "node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", + "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/ieee754": { @@ -3664,9 +3663,9 @@ } }, "node_modules/istanbul-lib-source-maps/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, "license": "MIT", "dependencies": { @@ -3719,16 +3718,16 @@ } }, "node_modules/jest": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", - "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.1.3.tgz", + "integrity": "sha512-Ry+p2+NLk6u8Agh5yVqELfUJvRfV51hhVBRIB5yZPY7mU0DGBmOuFG5GebZbMbm86cdQNK0fhJuDX8/1YorISQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.2.0", - "@jest/types": "30.2.0", + "@jest/core": "30.1.3", + "@jest/types": "30.0.5", "import-local": "^3.2.0", - "jest-cli": "30.2.0" + "jest-cli": "30.1.3" }, "bin": { "jest": "bin/jest.js" @@ -3746,14 +3745,14 @@ } }, "node_modules/jest-changed-files": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", - "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.5.tgz", + "integrity": "sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==", "dev": true, "license": "MIT", "dependencies": { "execa": "^5.1.1", - "jest-util": "30.2.0", + "jest-util": "30.0.5", "p-limit": "^3.1.0" }, "engines": { @@ -3761,29 +3760,29 @@ } }, "node_modules/jest-circus": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", - "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.1.3.tgz", + "integrity": "sha512-Yf3dnhRON2GJT4RYzM89t/EXIWNxKTpWTL9BfF3+geFetWP4XSvJjiU1vrWplOiUkmq8cHLiwuhz+XuUp9DscA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.2.0", - "@jest/expect": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/types": "30.2.0", + "@jest/environment": "30.1.2", + "@jest/expect": "30.1.2", + "@jest/test-result": "30.1.3", + "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "co": "^4.6.0", "dedent": "^1.6.0", "is-generator-fn": "^2.1.0", - "jest-each": "30.2.0", - "jest-matcher-utils": "30.2.0", - "jest-message-util": "30.2.0", - "jest-runtime": "30.2.0", - "jest-snapshot": "30.2.0", - "jest-util": "30.2.0", + "jest-each": "30.1.0", + "jest-matcher-utils": "30.1.2", + "jest-message-util": "30.1.0", + "jest-runtime": "30.1.3", + "jest-snapshot": "30.1.2", + "jest-util": "30.0.5", "p-limit": "^3.1.0", - "pretty-format": "30.2.0", + "pretty-format": "30.0.5", "pure-rand": "^7.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" @@ -3793,21 +3792,21 @@ } }, "node_modules/jest-cli": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", - "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.1.3.tgz", + "integrity": "sha512-G8E2Ol3OKch1DEeIBl41NP7OiC6LBhfg25Btv+idcusmoUSpqUkbrneMqbW9lVpI/rCKb/uETidb7DNteheuAQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/types": "30.2.0", + "@jest/core": "30.1.3", + "@jest/test-result": "30.1.3", + "@jest/types": "30.0.5", "chalk": "^4.1.2", "exit-x": "^0.2.2", "import-local": "^3.2.0", - "jest-config": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", + "jest-config": "30.1.3", + "jest-util": "30.0.5", + "jest-validate": "30.1.0", "yargs": "^17.7.2" }, "bin": { @@ -3826,34 +3825,34 @@ } }, "node_modules/jest-config": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", - "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.1.3.tgz", + "integrity": "sha512-M/f7gqdQEPgZNA181Myz+GXCe8jXcJsGjCMXUzRj22FIXsZOyHNte84e0exntOvdPaeh9tA0w+B8qlP2fAezfw==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", "@jest/get-type": "30.1.0", "@jest/pattern": "30.0.1", - "@jest/test-sequencer": "30.2.0", - "@jest/types": "30.2.0", - "babel-jest": "30.2.0", + "@jest/test-sequencer": "30.1.3", + "@jest/types": "30.0.5", + "babel-jest": "30.1.2", "chalk": "^4.1.2", "ci-info": "^4.2.0", "deepmerge": "^4.3.1", "glob": "^10.3.10", "graceful-fs": "^4.2.11", - "jest-circus": "30.2.0", - "jest-docblock": "30.2.0", - "jest-environment-node": "30.2.0", + "jest-circus": "30.1.3", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.1.2", "jest-regex-util": "30.0.1", - "jest-resolve": "30.2.0", - "jest-runner": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", + "jest-resolve": "30.1.3", + "jest-runner": "30.1.3", + "jest-util": "30.0.5", + "jest-validate": "30.1.0", "micromatch": "^4.0.8", "parse-json": "^5.2.0", - "pretty-format": "30.2.0", + "pretty-format": "30.0.5", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -3878,25 +3877,25 @@ } }, "node_modules/jest-diff": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", - "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.2.tgz", + "integrity": "sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/diff-sequences": "30.0.1", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.2.0" + "pretty-format": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-docblock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", - "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz", + "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", "dev": true, "license": "MIT", "dependencies": { @@ -3907,56 +3906,56 @@ } }, "node_modules/jest-each": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", - "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.1.0.tgz", + "integrity": "sha512-A+9FKzxPluqogNahpCv04UJvcZ9B3HamqpDNWNKDjtxVRYB8xbZLFuCr8JAJFpNp83CA0anGQFlpQna9Me+/tQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "@jest/types": "30.2.0", + "@jest/types": "30.0.5", "chalk": "^4.1.2", - "jest-util": "30.2.0", - "pretty-format": "30.2.0" + "jest-util": "30.0.5", + "pretty-format": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-environment-node": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", - "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.1.2.tgz", + "integrity": "sha512-w8qBiXtqGWJ9xpJIA98M0EIoq079GOQRQUyse5qg1plShUCQ0Ek1VTTcczqKrn3f24TFAgFtT+4q3aOXvjbsuA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.2.0", - "@jest/fake-timers": "30.2.0", - "@jest/types": "30.2.0", + "@jest/environment": "30.1.2", + "@jest/fake-timers": "30.1.2", + "@jest/types": "30.0.5", "@types/node": "*", - "jest-mock": "30.2.0", - "jest-util": "30.2.0", - "jest-validate": "30.2.0" + "jest-mock": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.1.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-haste-map": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", - "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.1.0.tgz", + "integrity": "sha512-JLeM84kNjpRkggcGpQLsV7B8W4LNUWz7oDNVnY1Vjj22b5/fAb3kk3htiD+4Na8bmJmjJR7rBtS2Rmq/NEcADg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.0.5", "@types/node": "*", "anymatch": "^3.1.3", "fb-watchman": "^2.0.2", "graceful-fs": "^4.2.11", "jest-regex-util": "30.0.1", - "jest-util": "30.2.0", - "jest-worker": "30.2.0", + "jest-util": "30.0.5", + "jest-worker": "30.1.0", "micromatch": "^4.0.8", "walker": "^1.0.8" }, @@ -3968,49 +3967,49 @@ } }, "node_modules/jest-leak-detector": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", - "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.1.0.tgz", + "integrity": "sha512-AoFvJzwxK+4KohH60vRuHaqXfWmeBATFZpzpmzNmYTtmRMiyGPVhkXpBqxUQunw+dQB48bDf4NpUs6ivVbRv1g==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "pretty-format": "30.2.0" + "pretty-format": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", - "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.1.2.tgz", + "integrity": "sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "jest-diff": "30.2.0", - "pretty-format": "30.2.0" + "jest-diff": "30.1.2", + "pretty-format": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-message-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", - "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.1.0.tgz", + "integrity": "sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.2.0", + "@jest/types": "30.0.5", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "micromatch": "^4.0.8", - "pretty-format": "30.2.0", + "pretty-format": "30.0.5", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -4019,15 +4018,15 @@ } }, "node_modules/jest-mock": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", - "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", + "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.0.5", "@types/node": "*", - "jest-util": "30.2.0" + "jest-util": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -4062,18 +4061,18 @@ } }, "node_modules/jest-resolve": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", - "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.1.3.tgz", + "integrity": "sha512-DI4PtTqzw9GwELFS41sdMK32Ajp3XZQ8iygeDMWkxlRhm7uUTOFSZFVZABFuxr0jvspn8MAYy54NxZCsuCTSOw==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", + "jest-haste-map": "30.1.0", "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.2.0", - "jest-validate": "30.2.0", + "jest-util": "30.0.5", + "jest-validate": "30.1.0", "slash": "^3.0.0", "unrs-resolver": "^1.7.11" }, @@ -4082,46 +4081,46 @@ } }, "node_modules/jest-resolve-dependencies": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", - "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.1.3.tgz", + "integrity": "sha512-DNfq3WGmuRyHRHfEet+Zm3QOmVFtIarUOQHHryKPc0YL9ROfgWZxl4+aZq/VAzok2SS3gZdniP+dO4zgo59hBg==", "dev": true, "license": "MIT", "dependencies": { "jest-regex-util": "30.0.1", - "jest-snapshot": "30.2.0" + "jest-snapshot": "30.1.2" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runner": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", - "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.1.3.tgz", + "integrity": "sha512-dd1ORcxQraW44Uz029TtXj85W11yvLpDuIzNOlofrC8GN+SgDlgY4BvyxJiVeuabA1t6idjNbX59jLd2oplOGQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.2.0", - "@jest/environment": "30.2.0", - "@jest/test-result": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", + "@jest/console": "30.1.2", + "@jest/environment": "30.1.2", + "@jest/test-result": "30.1.3", + "@jest/transform": "30.1.2", + "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "emittery": "^0.13.1", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", - "jest-docblock": "30.2.0", - "jest-environment-node": "30.2.0", - "jest-haste-map": "30.2.0", - "jest-leak-detector": "30.2.0", - "jest-message-util": "30.2.0", - "jest-resolve": "30.2.0", - "jest-runtime": "30.2.0", - "jest-util": "30.2.0", - "jest-watcher": "30.2.0", - "jest-worker": "30.2.0", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.1.2", + "jest-haste-map": "30.1.0", + "jest-leak-detector": "30.1.0", + "jest-message-util": "30.1.0", + "jest-resolve": "30.1.3", + "jest-runtime": "30.1.3", + "jest-util": "30.0.5", + "jest-watcher": "30.1.3", + "jest-worker": "30.1.0", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -4130,32 +4129,32 @@ } }, "node_modules/jest-runtime": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", - "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.1.3.tgz", + "integrity": "sha512-WS8xgjuNSphdIGnleQcJ3AKE4tBKOVP+tKhCD0u+Tb2sBmsU8DxfbBpZX7//+XOz81zVs4eFpJQwBNji2Y07DA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.2.0", - "@jest/fake-timers": "30.2.0", - "@jest/globals": "30.2.0", + "@jest/environment": "30.1.2", + "@jest/fake-timers": "30.1.2", + "@jest/globals": "30.1.2", "@jest/source-map": "30.0.1", - "@jest/test-result": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", + "@jest/test-result": "30.1.3", + "@jest/transform": "30.1.2", + "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "cjs-module-lexer": "^2.1.0", "collect-v8-coverage": "^1.0.2", "glob": "^10.3.10", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.2.0", - "jest-message-util": "30.2.0", - "jest-mock": "30.2.0", + "jest-haste-map": "30.1.0", + "jest-message-util": "30.1.0", + "jest-mock": "30.0.5", "jest-regex-util": "30.0.1", - "jest-resolve": "30.2.0", - "jest-snapshot": "30.2.0", - "jest-util": "30.2.0", + "jest-resolve": "30.1.3", + "jest-snapshot": "30.1.2", + "jest-util": "30.0.5", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -4164,9 +4163,9 @@ } }, "node_modules/jest-snapshot": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", - "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", + "version": "30.1.2", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.1.2.tgz", + "integrity": "sha512-4q4+6+1c8B6Cy5pGgFvjDy/Pa6VYRiGu0yQafKkJ9u6wQx4G5PqI2QR6nxTl43yy7IWsINwz6oT4o6tD12a8Dg==", "dev": true, "license": "MIT", "dependencies": { @@ -4175,20 +4174,20 @@ "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.2.0", + "@jest/expect-utils": "30.1.2", "@jest/get-type": "30.1.0", - "@jest/snapshot-utils": "30.2.0", - "@jest/transform": "30.2.0", - "@jest/types": "30.2.0", - "babel-preset-current-node-syntax": "^1.2.0", + "@jest/snapshot-utils": "30.1.2", + "@jest/transform": "30.1.2", + "@jest/types": "30.0.5", + "babel-preset-current-node-syntax": "^1.1.0", "chalk": "^4.1.2", - "expect": "30.2.0", + "expect": "30.1.2", "graceful-fs": "^4.2.11", - "jest-diff": "30.2.0", - "jest-matcher-utils": "30.2.0", - "jest-message-util": "30.2.0", - "jest-util": "30.2.0", - "pretty-format": "30.2.0", + "jest-diff": "30.1.2", + "jest-matcher-utils": "30.1.2", + "jest-message-util": "30.1.0", + "jest-util": "30.0.5", + "pretty-format": "30.0.5", "semver": "^7.7.2", "synckit": "^0.11.8" }, @@ -4210,13 +4209,13 @@ } }, "node_modules/jest-util": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", - "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.2.0", + "@jest/types": "30.0.5", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", @@ -4241,18 +4240,18 @@ } }, "node_modules/jest-validate": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", - "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.1.0.tgz", + "integrity": "sha512-7P3ZlCFW/vhfQ8pE7zW6Oi4EzvuB4sgR72Q1INfW9m0FGo0GADYlPwIkf4CyPq7wq85g+kPMtPOHNAdWHeBOaA==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "@jest/types": "30.2.0", + "@jest/types": "30.0.5", "camelcase": "^6.3.0", "chalk": "^4.1.2", "leven": "^3.1.0", - "pretty-format": "30.2.0" + "pretty-format": "30.0.5" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -4272,19 +4271,19 @@ } }, "node_modules/jest-watcher": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", - "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", + "version": "30.1.3", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.1.3.tgz", + "integrity": "sha512-6jQUZCP1BTL2gvG9E4YF06Ytq4yMb4If6YoQGRR6PpjtqOXSP3sKe2kqwB6SQ+H9DezOfZaSLnmka1NtGm3fCQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.2.0", - "@jest/types": "30.2.0", + "@jest/test-result": "30.1.3", + "@jest/types": "30.0.5", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "emittery": "^0.13.1", - "jest-util": "30.2.0", + "jest-util": "30.0.5", "string-length": "^4.0.2" }, "engines": { @@ -4292,15 +4291,15 @@ } }, "node_modules/jest-worker": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", - "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.1.0.tgz", + "integrity": "sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.2.0", + "jest-util": "30.0.5", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" }, @@ -4352,9 +4351,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -4493,6 +4492,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", "engines": { "node": ">= 0.4" } @@ -4501,6 +4501,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -4538,13 +4539,30 @@ } }, "node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "license": "MIT", "engines": { "node": ">= 0.6" } }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", @@ -4731,9 +4749,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", - "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true, "license": "MIT" }, @@ -4825,6 +4843,7 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "license": "MIT", "engines": { "node": ">= 0.4" }, @@ -5097,9 +5116,9 @@ } }, "node_modules/pretty-format": { - "version": "30.2.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", - "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", "dev": true, "license": "MIT", "dependencies": { @@ -5131,6 +5150,7 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { "@prisma/config": "6.16.2", "@prisma/engines": "6.16.2" @@ -5222,6 +5242,7 @@ "version": "6.14.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.1.0" }, @@ -5241,17 +5262,18 @@ } }, "node_modules/raw-body": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", - "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", + "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", + "license": "MIT", "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.6.3", - "unpipe": "1.0.0" + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.7.0", + "unpipe": "~1.0.0" }, "engines": { - "node": ">= 0.8" + "node": ">= 0.10" } }, "node_modules/rc9": { @@ -5400,7 +5422,8 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", @@ -5449,25 +5472,6 @@ } } }, - "node_modules/send/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/send/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/send/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -5519,6 +5523,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", @@ -5537,6 +5542,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "license": "MIT", "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" @@ -5552,6 +5558,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -5569,6 +5576,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "license": "MIT", "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", @@ -5799,9 +5807,9 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6040,6 +6048,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "license": "MIT", "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", @@ -6049,25 +6058,6 @@ "node": ">= 0.6" } }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-types": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", - "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", - "dependencies": { - "mime-db": "^1.54.0" - }, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/typedarray": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", @@ -6092,6 +6082,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "license": "MIT", "engines": { "node": ">= 0.8" } @@ -6356,9 +6347,9 @@ } }, "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index a09988c..989093f 100644 --- a/package.json +++ b/package.json @@ -14,12 +14,13 @@ "command-line-usage": "^7.0.3", "compression": "^1.8.1", "cors": "^2.8.5", - "express": "^5.0.0", + "express": "^5.2.1", "mqtt": "^5.14.1", - "protobufjs": "^7.5.4" + "protobufjs": "^7.5.4", + "ws": "^8.18.3" }, "devDependencies": { - "jest": "^30.2.0", + "jest": "^30.1.3", "prisma": "^6.16.2" } } diff --git a/prisma/migrations/20260106151912_add_edges/migration.sql b/prisma/migrations/20260106151912_add_edges/migration.sql new file mode 100644 index 0000000..113243f --- /dev/null +++ b/prisma/migrations/20260106151912_add_edges/migration.sql @@ -0,0 +1,23 @@ +-- CreateTable +CREATE TABLE `edges` ( + `id` BIGINT NOT NULL AUTO_INCREMENT, + `from_node_id` BIGINT NOT NULL, + `to_node_id` BIGINT NOT NULL, + `snr` INTEGER NOT NULL, + `from_latitude` INTEGER NULL, + `from_longitude` INTEGER NULL, + `to_latitude` INTEGER NULL, + `to_longitude` INTEGER NULL, + `packet_id` BIGINT NOT NULL, + `channel_id` VARCHAR(191) NULL, + `gateway_id` BIGINT NULL, + `source` VARCHAR(191) NOT NULL, + `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + `updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + + INDEX `edges_from_node_id_idx`(`from_node_id`), + INDEX `edges_to_node_id_idx`(`to_node_id`), + INDEX `edges_created_at_idx`(`created_at`), + INDEX `edges_from_node_id_to_node_id_idx`(`from_node_id`, `to_node_id`), + PRIMARY KEY (`id`) +) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ef96a9c..2a908dc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -347,4 +347,28 @@ model ChannelUtilizationStats { @@index([channel_id]) @@index([recorded_at]) @@map("channel_utilization_stats") +} + +model Edge { + id BigInt @id @default(autoincrement()) + from_node_id BigInt + to_node_id BigInt + snr Int + from_latitude Int? + from_longitude Int? + to_latitude Int? + to_longitude Int? + packet_id BigInt + channel_id String? + gateway_id BigInt? + source String + + created_at DateTime @default(now()) + updated_at DateTime @default(now()) @updatedAt + + @@index(from_node_id) + @@index(to_node_id) + @@index(created_at) + @@index([from_node_id, to_node_id]) + @@map("edges") } \ No newline at end of file diff --git a/screenshot.png b/screenshot.png index 13d17b2..25ee627 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/src/admin.js b/src/admin.js index e9fbbef..5889908 100644 --- a/src/admin.js +++ b/src/admin.js @@ -1,6 +1,7 @@ // node src/admin.js --purge-node-id 123 // node src/admin.js --purge-node-id '!AABBCCDD' +require('./utils/logger'); const commandLineArgs = require("command-line-args"); const commandLineUsage = require("command-line-usage"); diff --git a/src/index.js b/src/index.js index a51e465..48dbcc2 100644 --- a/src/index.js +++ b/src/index.js @@ -1,3 +1,4 @@ +require('./utils/logger'); const path = require('path'); const express = require('express'); const compression = require('compression'); @@ -154,6 +155,15 @@ app.get('/api', async (req, res) => { "time_to": "Only include traceroutes updated before this unix timestamp (milliseconds)" } }, + { + "path": "/api/v1/connections", + "description": "Aggregated edges between nodes from traceroutes", + "params": { + "node_id": "Only include connections involving this node id", + "time_from": "Only include edges created after this unix timestamp (milliseconds)", + "time_to": "Only include edges created before this unix timestamp (milliseconds)" + } + }, { "path": "/api/v1/nodes/:nodeId/position-history", "description": "Position history for a meshtastic node", @@ -219,6 +229,10 @@ app.get('/api/v1/nodes', async (req, res) => { where: { role: role, hardware_model: hardwareModel, + // Since we removed retention; only include nodes that have been updated in the last 30 days + updated_at: { + gte: new Date(Date.now() - 30 * 24 * 60 * 60 * 1000) // within last 30 days + } }, }); @@ -693,6 +707,194 @@ app.get('/api/v1/traceroutes', async (req, res) => { } }); +// Aggregated edges endpoint +// GET /api/v1/connections?node_id=...&time_from=...&time_to=... +app.get('/api/v1/connections', async (req, res) => { + try { + const nodeId = req.query.node_id ? parseInt(req.query.node_id) : undefined; + const timeFrom = req.query.time_from ? parseInt(req.query.time_from) : undefined; + const timeTo = req.query.time_to ? parseInt(req.query.time_to) : undefined; + + // Query edges from database + const edges = await prisma.edge.findMany({ + where: { + created_at: { + ...(timeFrom && { gte: new Date(timeFrom) }), + ...(timeTo && { lte: new Date(timeTo) }), + }, + // Only include edges where both nodes have positions + from_latitude: { not: null }, + from_longitude: { not: null }, + to_latitude: { not: null }, + to_longitude: { not: null }, + // If node_id is provided, filter edges where either from_node_id or to_node_id matches + ...(nodeId !== undefined && { + OR: [ + { from_node_id: nodeId }, + { to_node_id: nodeId }, + ], + }), + }, + orderBy: [ + { created_at: 'desc' }, + { packet_id: 'desc' }, + ], + }); + + // Collect all unique node IDs from edges + const nodeIds = new Set(); + for (const edge of edges) { + nodeIds.add(edge.from_node_id); + nodeIds.add(edge.to_node_id); + } + + // Fetch current positions for all nodes + const nodes = await prisma.node.findMany({ + where: { + node_id: { in: Array.from(nodeIds) }, + }, + select: { + node_id: true, + latitude: true, + longitude: true, + }, + }); + + // Create a map of current node positions + const nodePositions = new Map(); + for (const node of nodes) { + nodePositions.set(node.node_id, { + latitude: node.latitude, + longitude: node.longitude, + }); + } + + // Filter edges: only include edges where both nodes are still at the same location + const validEdges = edges.filter(edge => { + const fromCurrentPos = nodePositions.get(edge.from_node_id); + const toCurrentPos = nodePositions.get(edge.to_node_id); + + // Skip if either node doesn't exist or doesn't have a current position + if (!fromCurrentPos || !toCurrentPos || + fromCurrentPos.latitude === null || fromCurrentPos.longitude === null || + toCurrentPos.latitude === null || toCurrentPos.longitude === null) { + return false; + } + + // Check if stored positions match current positions + const fromMatches = fromCurrentPos.latitude === edge.from_latitude && + fromCurrentPos.longitude === edge.from_longitude; + const toMatches = toCurrentPos.latitude === edge.to_latitude && + toCurrentPos.longitude === edge.to_longitude; + + return fromMatches && toMatches; + }); + + // Normalize node pairs: always use min/max to treat A->B and B->A as same connection + const connectionsMap = new Map(); + + for (const edge of validEdges) { + const nodeA = edge.from_node_id < edge.to_node_id ? edge.from_node_id : edge.to_node_id; + const nodeB = edge.from_node_id < edge.to_node_id ? edge.to_node_id : edge.from_node_id; + const key = `${nodeA}-${nodeB}`; + + if (!connectionsMap.has(key)) { + connectionsMap.set(key, { + node_a: nodeA, + node_b: nodeB, + direction_ab: [], // A -> B edges + direction_ba: [], // B -> A edges + }); + } + + const connection = connectionsMap.get(key); + const isAB = edge.from_node_id === nodeA; + + // Add edge to appropriate direction + if (isAB) { + connection.direction_ab.push({ + snr: edge.snr, + snr_db: edge.snr / 4, // Convert to dB + created_at: edge.created_at, + packet_id: edge.packet_id, + source: edge.source, + }); + } else { + connection.direction_ba.push({ + snr: edge.snr, + snr_db: edge.snr / 4, + created_at: edge.created_at, + packet_id: edge.packet_id, + source: edge.source, + }); + } + } + + // Aggregate each connection + const connections = Array.from(connectionsMap.values()).map(conn => { + // Deduplicate edges by packet_id for each direction (keep first occurrence, which is most recent) + const dedupeByPacketId = (edges) => { + const seen = new Set(); + return edges.filter(edge => { + if (seen.has(edge.packet_id)) { + return false; + } + seen.add(edge.packet_id); + return true; + }); + }; + + const deduplicatedAB = dedupeByPacketId(conn.direction_ab); + const deduplicatedBA = dedupeByPacketId(conn.direction_ba); + + // Calculate average SNR for A->B (using deduplicated edges) + const avgSnrAB = deduplicatedAB.length > 0 + ? deduplicatedAB.reduce((sum, e) => sum + e.snr_db, 0) / deduplicatedAB.length + : null; + + // Calculate average SNR for B->A (using deduplicated edges) + const avgSnrBA = deduplicatedBA.length > 0 + ? deduplicatedBA.reduce((sum, e) => sum + e.snr_db, 0) / deduplicatedBA.length + : null; + + // Get last 5 edges for each direction (already sorted by created_at DESC, packet_id DESC, now deduplicated) + const last5AB = deduplicatedAB.slice(0, 5); + const last5BA = deduplicatedBA.slice(0, 5); + + // Determine worst average SNR + const worstAvgSnrDb = [avgSnrAB, avgSnrBA] + .filter(v => v !== null) + .reduce((min, val) => val < min ? val : min, Infinity); + + return { + node_a: conn.node_a, + node_b: conn.node_b, + direction_ab: { + avg_snr_db: avgSnrAB, + last_5_edges: last5AB, + total_count: deduplicatedAB.length, // Use deduplicated count + }, + direction_ba: { + avg_snr_db: avgSnrBA, + last_5_edges: last5BA, + total_count: deduplicatedBA.length, // Use deduplicated count + }, + worst_avg_snr_db: worstAvgSnrDb !== Infinity ? worstAvgSnrDb : null, + }; + }).filter(conn => conn.worst_avg_snr_db !== null); // Only return connections with at least one direction + + res.json({ + connections: connections, + }); + + } catch (err) { + console.error(err); + res.status(500).json({ + message: "Something went wrong, try again later.", + }); + } +}); + app.get('/api/v1/nodes/:nodeId/position-history', async (req, res) => { try { diff --git a/src/mqtt.js b/src/mqtt.js index 4852907..67cb9dd 100644 --- a/src/mqtt.js +++ b/src/mqtt.js @@ -1,3 +1,4 @@ +require('./utils/logger'); const crypto = require("crypto"); const path = require("path"); const mqtt = require("mqtt"); @@ -983,7 +984,12 @@ client.on("message", async (topic, message) => { }, }); } catch (e) { - console.error(e); + // Ignore MySQL error 1020 "Record has changed since last read" - this is a race condition + // that occurs when multiple packets arrive concurrently for the same node + const errorMessage = e.message || String(e); + if (!errorMessage.includes('Record has changed since last read')) { + console.error(e); + } } // Keep track of the names a node has been using. @@ -1006,7 +1012,12 @@ client.on("message", async (topic, message) => { } }); } catch (e) { - console.error(e); + // Ignore MySQL error 1020 "Record has changed since last read" - this is a race condition + // that occurs when multiple packets arrive concurrently for the same node + const errorMessage = e.message || String(e); + if (!errorMessage.includes('Record has changed since last read')) { + console.error(e); + } } } @@ -1083,6 +1094,70 @@ client.on("message", async (topic, message) => { console.error(e); } + // Extract edges from neighbour info + try { + const toNodeId = envelope.packet.from; + const neighbors = neighbourInfo.neighbors || []; + const packetId = envelope.packet.id; + const channelId = envelope.channelId; + const gatewayId = envelope.gatewayId ? convertHexIdToNumericId(envelope.gatewayId) : null; + const edgesToCreate = []; + + for(const neighbour of neighbors) { + // Skip if no node ID + if(!neighbour.nodeId) { + continue; + } + + // Skip if SNR is invalid (0 or null/undefined) + // Note: SNR can be negative, so we check for 0 specifically + if(neighbour.snr === 0 || neighbour.snr == null) { + continue; + } + + const fromNodeId = neighbour.nodeId; + const snr = neighbour.snr; + + // Fetch node positions from Node table + const [fromNode, toNode] = await Promise.all([ + prisma.node.findUnique({ + where: { node_id: fromNodeId }, + select: { latitude: true, longitude: true }, + }), + prisma.node.findUnique({ + where: { node_id: toNodeId }, + select: { latitude: true, longitude: true }, + }), + ]); + + // Create edge record + edgesToCreate.push({ + from_node_id: fromNodeId, + to_node_id: toNodeId, + snr: snr, + from_latitude: fromNode?.latitude ?? null, + from_longitude: fromNode?.longitude ?? null, + to_latitude: toNode?.latitude ?? null, + to_longitude: toNode?.longitude ?? null, + packet_id: packetId, + channel_id: channelId, + gateway_id: gatewayId, + source: "NEIGHBORINFO_APP", + }); + } + + // Bulk insert edges + if(edgesToCreate.length > 0) { + await prisma.edge.createMany({ + data: edgesToCreate, + skipDuplicates: true, // Skip if exact duplicate exists + }); + } + } catch (e) { + // Log error but don't crash - edge extraction is non-critical + console.error("Error extracting edges from neighbour info:", e); + } + // don't store all neighbour infos, but we want to update the existing node above if(!collectNeighbourInfo){ return; @@ -1325,6 +1400,160 @@ client.on("message", async (topic, message) => { console.error(e); } + // Extract edges from traceroute (only for response packets) + if(!envelope.packet.decoded.wantResponse) { + try { + const route = routeDiscovery.route || []; + const snrTowards = routeDiscovery.snrTowards || []; + const originNodeId = envelope.packet.to; + const destinationNodeId = envelope.packet.from; + const packetId = envelope.packet.id; + const channelId = envelope.channelId; + const gatewayId = envelope.gatewayId ? convertHexIdToNumericId(envelope.gatewayId) : null; + + // Determine number of edges: route.length + 1 + const numEdges = route.length + 1; + const edgesToCreate = []; + + // Extract edges from the route path + for(let i = 0; i < numEdges; i++) { + // Get SNR for this edge + if(i >= snrTowards.length) { + // Array length mismatch - skip this edge + continue; + } + + const snr = snrTowards[i]; + + // Skip if SNR is -128 (no SNR recorded) + if(snr === -128) { + continue; + } + + // Determine from_node and to_node + let fromNodeId, toNodeId; + + if(route.length === 0) { + // Empty route: direct connection (to -> from) + fromNodeId = originNodeId; + toNodeId = destinationNodeId; + } else if(i === 0) { + // First edge: origin -> route[0] + fromNodeId = originNodeId; + toNodeId = route[0]; + } else if(i === route.length) { + // Last edge: route[route.length-1] -> destination + fromNodeId = route[route.length - 1]; + toNodeId = destinationNodeId; + } else { + // Middle edge: route[i-1] -> route[i] + fromNodeId = route[i - 1]; + toNodeId = route[i]; + } + + // Fetch node positions from Node table + const [fromNode, toNode] = await Promise.all([ + prisma.node.findUnique({ + where: { node_id: fromNodeId }, + select: { latitude: true, longitude: true }, + }), + prisma.node.findUnique({ + where: { node_id: toNodeId }, + select: { latitude: true, longitude: true }, + }), + ]); + + // Create edge record (skip if nodes don't exist, but still create edge with null positions) + edgesToCreate.push({ + from_node_id: fromNodeId, + to_node_id: toNodeId, + snr: snr, + from_latitude: fromNode?.latitude ?? null, + from_longitude: fromNode?.longitude ?? null, + to_latitude: toNode?.latitude ?? null, + to_longitude: toNode?.longitude ?? null, + packet_id: packetId, + channel_id: channelId, + gateway_id: gatewayId, + source: "TRACEROUTE_APP", + }); + } + + // Extract edges from route_back path + const routeBack = routeDiscovery.routeBack || []; + const snrBack = routeDiscovery.snrBack || []; + + if(routeBack.length > 0) { + // Number of edges in route_back equals route_back.length + for(let i = 0; i < routeBack.length; i++) { + // Get SNR for this edge + if(i >= snrBack.length) { + // Array length mismatch - skip this edge + continue; + } + + const snr = snrBack[i]; + + // Skip if SNR is -128 (no SNR recorded) + if(snr === -128) { + continue; + } + + // Determine from_node and to_node + let fromNodeId, toNodeId; + + if(i === 0) { + // First edge: from -> route_back[0] + fromNodeId = destinationNodeId; // 'from' in the packet + toNodeId = routeBack[0]; + } else { + // Subsequent edges: route_back[i-1] -> route_back[i] + fromNodeId = routeBack[i - 1]; + toNodeId = routeBack[i]; + } + + // Fetch node positions from Node table + const [fromNode, toNode] = await Promise.all([ + prisma.node.findUnique({ + where: { node_id: fromNodeId }, + select: { latitude: true, longitude: true }, + }), + prisma.node.findUnique({ + where: { node_id: toNodeId }, + select: { latitude: true, longitude: true }, + }), + ]); + + // Create edge record + edgesToCreate.push({ + from_node_id: fromNodeId, + to_node_id: toNodeId, + snr: snr, + from_latitude: fromNode?.latitude ?? null, + from_longitude: fromNode?.longitude ?? null, + to_latitude: toNode?.latitude ?? null, + to_longitude: toNode?.longitude ?? null, + packet_id: packetId, + channel_id: channelId, + gateway_id: gatewayId, + source: "TRACEROUTE_APP", + }); + } + } + + // Bulk insert edges + if(edgesToCreate.length > 0) { + await prisma.edge.createMany({ + data: edgesToCreate, + skipDuplicates: true, // Skip if exact duplicate exists + }); + } + } catch (e) { + // Log error but don't crash - edge extraction is non-critical + console.error("Error extracting edges from traceroute:", e); + } + } + } else if(portnum === 73) { @@ -1429,6 +1658,7 @@ client.on("message", async (topic, message) => { || portnum === 0 // ignore UNKNOWN_APP || portnum === 1 // ignore TEXT_MESSAGE_APP || portnum === 5 // ignore ROUTING_APP + || portnum === 6 // ignore ADMIN_APP || portnum === 34 // ignore PAXCOUNTER_APP || portnum === 65 // ignore STORE_FORWARD_APP || portnum === 66 // ignore RANGE_TEST_APP diff --git a/src/protobufs b/src/protobufs index 46b81e8..c2e45a3 160000 --- a/src/protobufs +++ b/src/protobufs @@ -1 +1 @@ -Subproject commit 46b81e822af1b8e408f437092337f129dee693e6 +Subproject commit c2e45a3fc9cda6aedb72ad3b5b88fcccfa78073e diff --git a/src/public/assets/css/styles.css b/src/public/assets/css/styles.css new file mode 100644 index 0000000..8811cad --- /dev/null +++ b/src/public/assets/css/styles.css @@ -0,0 +1,115 @@ +/* used to prevent ui flicker before vuejs loads */ +[v-cloak] { + display: none; +} + +.icon-longfast { + background-color: #009016; + border-radius: 25px; + border: 1px solid #2C2D3C; +} + +.icon-mediumfast { + background-color: #326be7; + border-radius: 25px; + border: 1px solid #2C2D3C; +} + +.icon-shortslow { + background-color: #0077e6; + border-radius: 25px; + border: 1px solid #2C2D3C; +} + +.icon-mqtt-connected { + background-color: #2563eb; /* Change to use same color as disconnected // #16a34a; */ + border-radius: 25px; + border: 1px solid #2C2D3C; +} + +.icon-mqtt-disconnected { + background-color: #2563eb; + border-radius: 25px; + border: 1px solid #2C2D3C; +} + +.icon-offline { + background-color: #e2286c; + border-radius: 25px; + border: 1px solid #2C2D3C; +} + +.icon-position-history { + background-color: #a855f7; + border-radius: 25px; + border: 1px solid #2C2D3C; +} + +.icon-traceroute-start { + background-color: #16a34a; /* green */ + border-radius: 25px; + border: 1px solid #2C2D3C; +} + +.icon-traceroute-end { + background-color: #dc2626; /* red */ + border-radius: 25px; + border: 1px solid #2C2D3C; +} + +.waypoint-label { + font-size: 26px; + background-color: transparent; +} + +.link { + color: #2563eb; +} + +.link:hover { + text-decoration: underline; +} + +.tooltip { + position: relative; + display: inline-block; +} + +.tooltip .tooltip-text { + visibility: hidden; + width: 80px; + background-color: black; + color: #fff; + text-align: center; + padding: 4px 0; + border-radius: 6px; + position: absolute; + z-index: 10000; + top: 100%; + left: 50%; + margin-top: 8px; + margin-left: -40px; /* Use half of the width (120/2 = 60), to center the tooltip */ +} + +.tooltip .tooltip-text::after { + content: " "; + position: absolute; + bottom: 100%; /* At the top of the tooltip */ + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent transparent black transparent; +} + +.tooltip:hover .tooltip-text { + visibility: visible; +} + +.z-search { + z-index: 1001; +} + +.z-sidebar { + z-index: 1002; +} \ No newline at end of file diff --git a/src/public/assets/js/app.js b/src/public/assets/js/app.js new file mode 100644 index 0000000..86b4542 --- /dev/null +++ b/src/public/assets/js/app.js @@ -0,0 +1,956 @@ +Vue.createApp({ + data() { + return { + + isShowingAnnouncement: this.shouldShowAnnouncement(), + + configNodesMaxAgeInSeconds: window.getConfigNodesMaxAgeInSeconds(), + configNodesOfflineAgeInSeconds: window.getConfigNodesOfflineAgeInSeconds(), + configWaypointsMaxAgeInSeconds: window.getConfigWaypointsMaxAgeInSeconds(), + configConnectionsMaxDistanceInMeters: window.getConfigConnectionsMaxDistanceInMeters(), + configZoomLevelGoToNode: window.getConfigZoomLevelGoToNode(), + configAutoUpdatePositionInUrl: window.getConfigAutoUpdatePositionInUrl(), + configEnableMapAnimations: window.getConfigEnableMapAnimations(), + configTemperatureFormat: window.getConfigTemperatureFormat(), + configConnectionsTimePeriodInSeconds: window.getConfigConnectionsTimePeriodInSeconds(), + configConnectionsColoredLines: window.getConfigConnectionsColoredLines(), + configConnectionsBidirectionalOnly: window.getConfigConnectionsBidirectionalOnly(), + configConnectionsMinSnrDb: window.getConfigConnectionsMinSnrDb(), + configConnectionsBidirectionalMinSnr: window.getConfigConnectionsBidirectionalMinSnr(), + + isShowingHardwareModels: false, + hardwareModelStats: null, + + isShowingInfoModal: this.shouldShowInfoModal(), + isShowingMobileSearch: false, + isShowingSettings: false, + + nodes: [], + searchText: "", + + selectedNode: null, + selectedNodeDeviceMetrics: [], + selectedNodeEnvironmentMetrics: [], + selectedNodePowerMetrics: [], + selectedNodeMqttMetrics: [], + selectedNodeTraceroutes: [], + + deviceMetricsTimeRange: "7d", + environmentMetricsTimeRange: "7d", + powerMetricsTimeRange: "7d", + + isPositionHistoryModalExpanded: true, + positionHistoryDateTimeFrom: null, + positionHistoryDateTimeTo: null, + selectedNodePositionHistory: [], + selectedNodeToShowPositionHistory: null, + selectedNodePositionHistoryMarkers: [], + selectedNodePositionHistoryPolyLines: [], + + selectedTraceRoute: null, + tracerouteEdges: [], + + selectedNodeToShowConnections: null, + + moment: window.moment, + + }; + }, + mounted: function() { + + // load data + this.loadHardwareModelStats(); + + // handle map click callback from outside of vue + window._onMapClick = () => { + this.searchText = ""; + this.isShowingMobileSearch = false; + }; + + // handle node callback from outside of vue + window._onNodeClick = (node) => { + this.selectedNode = node; + this.loadNodeDeviceMetrics(node.node_id); + this.loadNodeEnvironmentMetrics(node.node_id); + this.loadNodePowerMetrics(node.node_id); + this.loadNodeMqttMetrics(node.node_id); + this.loadNodeTraceroutes(node.node_id); + //this.loadNodePositionHistory(node.node_id); + }; + + // handle node callback from outside of vue + window._onShowNodeConnectionsClick = (node) => { + this.selectedNodeToShowConnections = node; + }; + + // handle nodes updated callback from outside of vue + window._onNodesUpdated = (nodes) => { + this.nodes = nodes; + }; + + }, + methods: { + getAnnouncementId: function() { + // change this when making a new announcement + return "1"; + }, + shouldShowAnnouncement: function() { + const lastSeenAnnouncementId = window.localStorage.getItem("last-seen-announcement-id"); + return lastSeenAnnouncementId?.toString() !== this.getAnnouncementId(); + }, + dismissAnnouncement: function() { + window.localStorage.setItem("last-seen-announcement-id", this.getAnnouncementId()); + this.isShowingAnnouncement = false; + }, + shouldShowInfoModal: function() { + return !window.getConfigHasSeenInfoModal() + && !window.isMobile(); + }, + loadHardwareModelStats: function() { + window.axios.get('/api/v1/stats/hardware-models').then((response) => { + this.hardwareModelStats = response.data.hardware_model_stats; + }).catch((error) => { + // do nothing + }); + }, + loadNodeDeviceMetrics: function(nodeId) { + + // calculate unix timestamps in milliseconds for supported time ranges + const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000); + const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000); + const sevenDaysAgoInMilliseconds = new Date().getTime() - (604800 * 1000); + const thirtyDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000 * 10); + + // determine how long back to load device metrics from + var timeFrom = threeDaysAgoInMilliseconds; + switch(this.deviceMetricsTimeRange){ + case "1d": { + timeFrom = oneDayAgoInMilliseconds; + break; + } + case "3d": { + timeFrom = threeDaysAgoInMilliseconds; + break; + } + case "7d": { + timeFrom = sevenDaysAgoInMilliseconds; + break; + } + case "30d": { + timeFrom = thirtyDaysAgoInMilliseconds; + break; + } + } + + window.axios.get(`/api/v1/nodes/${nodeId}/device-metrics`, { + params: { + time_from: timeFrom, + }, + }).then((response) => { + // reverse response, as it's newest to oldest, but we want oldest to newest + this.selectedNodeDeviceMetrics = response.data.device_metrics.reverse(); + this.renderDeviceMetricCharts(); + }).catch(() => { + this.selectedNodeDeviceMetrics = []; + this.renderDeviceMetricCharts(); + }); + }, + loadNodeEnvironmentMetrics: function(nodeId) { + + // calculate unix timestamps in milliseconds for supported time ranges + const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000); + const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000); + const sevenDaysAgoInMilliseconds = new Date().getTime() - (604800 * 1000); + const thirtyDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000 * 10); + + // determine how long back to load environment metrics from + var timeFrom = threeDaysAgoInMilliseconds; + switch(this.environmentMetricsTimeRange){ + case "1d": { + timeFrom = oneDayAgoInMilliseconds; + break; + } + case "3d": { + timeFrom = threeDaysAgoInMilliseconds; + break; + } + case "7d": { + timeFrom = sevenDaysAgoInMilliseconds; + break; + } + case "30d": { + timeFrom = thirtyDaysAgoInMilliseconds; + break; + } + } + + window.axios.get(`/api/v1/nodes/${nodeId}/environment-metrics`, { + params: { + time_from: timeFrom, + }, + }).then((response) => { + // reverse response, as it's newest to oldest, but we want oldest to newest + this.selectedNodeEnvironmentMetrics = response.data.environment_metrics.reverse(); + this.renderEnvironmentMetricCharts(); + }).catch(() => { + this.selectedNodeEnvironmentMetrics = []; + this.renderEnvironmentMetricCharts(); + }); + }, + loadNodePowerMetrics: function(nodeId) { + + // calculate unix timestamps in milliseconds for supported time ranges + const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000); + const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000); + const sevenDaysAgoInMilliseconds = new Date().getTime() - (604800 * 1000); + const thirtyDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000 * 10); + + // determine how long back to load power metrics from + var timeFrom = threeDaysAgoInMilliseconds; + switch(this.powerMetricsTimeRange){ + case "1d": { + timeFrom = oneDayAgoInMilliseconds; + break; + } + case "3d": { + timeFrom = threeDaysAgoInMilliseconds; + break; + } + case "7d": { + timeFrom = sevenDaysAgoInMilliseconds; + break; + } + case "30d": { + timeFrom = thirtyDaysAgoInMilliseconds; + break; + } + } + + window.axios.get(`/api/v1/nodes/${nodeId}/power-metrics`, { + params: { + time_from: timeFrom, + }, + }).then((response) => { + // reverse response, as it's newest to oldest, but we want oldest to newest + this.selectedNodePowerMetrics = response.data.power_metrics.reverse(); + this.renderPowerMetricCharts(); + }).catch(() => { + this.selectedNodePowerMetrics = []; + this.renderPowerMetricCharts(); + }); + }, + loadNodeMqttMetrics: function(nodeId) { + this.selectedNodeMqttMetrics = []; + window.axios.get(`/api/v1/nodes/${nodeId}/mqtt-metrics`).then((response) => { + this.selectedNodeMqttMetrics = response.data.mqtt_metrics; + }).catch(() => { + // do nothing + }); + }, + loadNodeTraceroutes: function(nodeId) { + this.selectedNodeTraceroutes = []; + window.axios.get(`/api/v1/nodes/${nodeId}/traceroutes`, { + params: { + count: 5, + }, + }).then((response) => { + this.selectedNodeTraceroutes = response.data.traceroutes; + }).catch(() => { + // do nothing + }); + }, + loadNodePositionHistory: function(nodeId) { + this.selectedNodePositionHistory = []; + window.axios.get(`/api/v1/nodes/${nodeId}/position-history`, { + params: { + // parse from datetime-local format, and send as unix timestamp in milliseconds + time_from: moment(this.positionHistoryDateTimeFrom, "YYYY-MM-DDTHH:mm").format("x"), + time_to: moment(this.positionHistoryDateTimeTo, "YYYY-MM-DDTHH:mm").format("x"), + }, + }).then((response) => { + this.selectedNodePositionHistory = response.data.position_history; + if(this.selectedNodeToShowPositionHistory != null){ + clearAllPositionHistory(); + onPositionHistoryUpdated(response.data.position_history); + } + + }).catch(() => { + // do nothing + }); + }, + renderDeviceMetricCharts: function() { + try { + this.updateDeviceMetricsChart(); + } catch(e) { + console.log(e); + } + }, + updateDeviceMetricsChart: function() { + + // destroy existing chart + const chartElementId = "deviceMetricsChart"; + const existingChart = window.Chart.getChart(chartElementId); + if(existingChart != null){ + existingChart.destroy(); + } + + // get chart element + const chartElement = window.document.getElementById(chartElementId); + if(!chartElement){ + return; + } + + // create chart data + const labels = []; + const batteryMetrics = []; + const channelUtilizationMetrics = []; + const airUtilTxMetrics = []; + for(const deviceMetric of this.selectedNodeDeviceMetrics){ + labels.push(moment(deviceMetric.created_at)); + batteryMetrics.push(deviceMetric.battery_level); + channelUtilizationMetrics.push(deviceMetric.channel_utilization); + airUtilTxMetrics.push(deviceMetric.air_util_tx); + } + + // create chart + new window.Chart(chartElement, { + type: 'line', + data: { + labels: labels, + datasets: [ + { + label: 'Battery Level', + borderColor: '#3b82f6', + backgroundColor: '#3b82f6', + pointStyle: false, // no points + fill: false, + data: batteryMetrics, + }, + { + label: 'Channel Util', + borderColor: '#22c55e', + backgroundColor: '#22c55e', + showLine: false, // no lines between points + fill: false, + data: channelUtilizationMetrics, + }, + { + label: 'Air Util TX', + borderColor: '#f97316', + backgroundColor: '#f97316', + showLine: false, // no lines between points + fill: false, + data: airUtilTxMetrics, + + }, + ], + }, + options: { + responsive: true, + borderWidth: 2, + elements: { + point: { + radius: 2, + }, + }, + scales: { + x: { + position: 'top', + type: 'time', + time: { + unit: 'day', + displayFormats: { + day: 'MMM DD', // Jan 01 + }, + }, + }, + y: { + min: 0, + max: 101, // 101 is "Plugged In", need to include for tooltip to work + ticks: { + callback: (label) => `${label}%`, + }, + }, + }, + plugins: { + legend: { + display: false, + }, + tooltip: { + mode: "index", + intersect: false, + callbacks: { + label: (item) => { + return `${item.dataset.label}: ${item.formattedValue}%`; + }, + }, + }, + }, + } + }); + + }, + renderEnvironmentMetricCharts: function() { + try { + this.updateEnvironmentMetricsChart(); + } catch(e) { + console.log(e); + } + }, + updateEnvironmentMetricsChart: function() { + + // destroy existing chart + const chartElementId = "environmentMetricsChart"; + const existingChart = window.Chart.getChart(chartElementId); + if(existingChart != null){ + existingChart.destroy(); + } + + // get chart element + const chartElement = window.document.getElementById(chartElementId); + if(!chartElement){ + return; + } + + // create chart data + const labels = []; + const temperatureMetrics = []; + const relativeHumidityMetrics = []; + const barometricPressureMetrics = []; + const iaqMetrics = []; + for(const deviceMetric of this.selectedNodeEnvironmentMetrics){ + labels.push(moment(deviceMetric.created_at)); + temperatureMetrics.push(deviceMetric.temperature); + relativeHumidityMetrics.push(deviceMetric.relative_humidity); + barometricPressureMetrics.push(deviceMetric.barometric_pressure); + iaqMetrics.push(deviceMetric.iaq); + } + + // create chart + new window.Chart(chartElement, { + type: 'line', + data: { + labels: labels, + datasets: [ + { + label: 'Temperature', + suffix: '°C', + borderColor: '#3b82f6', + backgroundColor: '#3b82f6', + pointStyle: false, // no points + fill: false, + data: temperatureMetrics, + yAxisID: 'y', + }, + { + label: 'Humidity', + suffix: '%', + borderColor: '#22c55e', + backgroundColor: '#22c55e', + pointStyle: false, // no points + fill: false, + data: relativeHumidityMetrics, + yAxisID: 'y', + }, + { + label: 'Pressure', + suffix: 'hPa', + borderColor: '#f97316', + backgroundColor: '#f97316', + pointStyle: false, // no points + fill: false, + data: barometricPressureMetrics, + yAxisID: 'y1', + + }, + { + label: 'IAQ', + suffix: 'IAQ', + borderColor: '#f472b6', + backgroundColor: '#f472b6', + pointStyle: false, // no points + fill: false, + data: iaqMetrics, + yAxisID: 'yIAQ', + + }, + ], + }, + options: { + responsive: true, + borderWidth: 2, + spanGaps: 1000 * 60 * 60 * 24, // only show lines between metrics with a 24 hour or less gap + elements: { + point: { + radius: 2, + }, + }, + scales: { + x: { + position: 'top', + type: 'time', + time: { + unit: 'day', + displayFormats: { + day: 'MMM DD', // Jan 01 + }, + }, + }, + y: { + min: -20, + max: 100, + }, + y1: { + min: 800, + max: 1100, + ticks: { + stepSize: 10, + callback: (label) => `${label} hPa`, + }, + position: 'right', + grid: { + drawOnChartArea: false, // only want the grid lines for one axis to show up + }, + }, + yIAQ: { + type: 'linear', + display: false, + }, + }, + plugins: { + legend: { + display: false, + }, + tooltip: { + mode: "index", + intersect: false, + callbacks: { + label: (item) => { + return `${item.dataset.label}: ${item.formattedValue}${item.dataset.suffix}`; + }, + }, + }, + }, + } + }); + + }, + renderPowerMetricCharts: function() { + try { + this.updatePowerMetricsChart(); + } catch(e) { + console.log(e); + } + }, + updatePowerMetricsChart: function() { + + // destroy existing chart + const chartElementId = "powerMetricsChart"; + const existingChart = window.Chart.getChart(chartElementId); + if(existingChart != null){ + existingChart.destroy(); + } + + // get chart element + const chartElement = window.document.getElementById(chartElementId); + if(!chartElement){ + return; + } + + // create chart data + const labels = []; + const channel1VoltageReadings = []; + const channel2VoltageReadings = []; + const channel3VoltageReadings = []; + const channel1CurrentReadings = []; + const channel2CurrentReadings = []; + const channel3CurrentReadings = []; + for(const powerMetric of this.selectedNodePowerMetrics){ + labels.push(moment(powerMetric.created_at)); + channel1VoltageReadings.push(powerMetric.ch1_voltage); + channel2VoltageReadings.push(powerMetric.ch2_voltage); + channel3VoltageReadings.push(powerMetric.ch3_voltage); + channel1CurrentReadings.push(powerMetric.ch1_current); + channel2CurrentReadings.push(powerMetric.ch2_current); + channel3CurrentReadings.push(powerMetric.ch3_current); + } + + // create chart + new window.Chart(chartElement, { + type: 'line', + data: { + labels: labels, + datasets: [ + { + label: 'Ch1 Voltage', + suffix: "V", + borderColor: '#3b82f6', + backgroundColor: '#3b82f6', + pointStyle: false, // no points + fill: false, + data: channel1VoltageReadings, + yAxisID: 'y', + }, + { + label: 'Ch2 Voltage', + suffix: "V", + borderColor: '#22c55e', + backgroundColor: '#22c55e', + pointStyle: false, // no points + fill: false, + data: channel2VoltageReadings, + yAxisID: 'y', + }, + { + label: 'Ch3 Voltage', + suffix: "V", + borderColor: '#f97316', + backgroundColor: '#f97316', + pointStyle: false, // no points + fill: false, + data: channel3VoltageReadings, + yAxisID: 'y', + }, + { + label: 'Ch1 Current', + suffix: "mA", + borderColor: '#93c5fd', + backgroundColor: '#93c5fd', + pointStyle: false, // no points + fill: false, + data: channel1CurrentReadings, + yAxisID: 'y1', + }, + { + label: 'Ch2 Current', + suffix: "mA", + borderColor: '#86efac', + backgroundColor: '#86efac', + pointStyle: false, // no points + fill: false, + data: channel2CurrentReadings, + yAxisID: 'y1', + }, + { + label: 'Ch3 Current', + suffix: "mA", + borderColor: '#fdba74', + backgroundColor: '#fdba74', + pointStyle: false, // no points + fill: false, + data: channel3CurrentReadings, + yAxisID: 'y1', + }, + ], + }, + options: { + responsive: true, + borderWidth: 2, + spanGaps: 1000 * 60 * 60 * 3, // only show lines between metrics with a 3 hour or less gap + elements: { + point: { + radius: 2, + }, + }, + scales: { + x: { + position: 'top', + type: 'time', + time: { + unit: 'day', + displayFormats: { + day: 'MMM DD', // Jan 01 + }, + }, + }, + y: { + min: 0, + suggestedMax: 6, + ticks: { + callback: (label) => `${label}V`, + }, + }, + y1: { + suggestedMin: -50, + suggestedMax: 50, + ticks: { + stepSize: 50, + callback: (label) => `${label}mA`, + }, + position: 'right', + grid: { + drawOnChartArea: false, // only want the grid lines for one axis to show up + }, + }, + }, + plugins: { + legend: { + display: false, + }, + tooltip: { + mode: "index", + intersect: false, + callbacks: { + label: (item) => { + return `${item.dataset.label}: ${item.formattedValue}${item.dataset.suffix}`; + }, + }, + }, + }, + } + }); + + }, + showTraceRoute: function(traceroute) { + this.selectedTraceRoute = traceroute; + }, + findNodeById: function(id) { + return window.findNodeById(id); + }, + findNodeMarkerById: function(id) { + return window.findNodeMarkerById(id); + }, + onSearchResultNodeClick: function(node) { + + // clear search + this.searchText = ""; + + // hide search + this.isShowingMobileSearch = false; + + // go to node + if(window.goToNode(node.node_id)){ + return; + } + + // fallback to showing node details since we can't go to the node + window.showNodeDetails(node.node_id); + + }, + dismissInfoModal: function() { + this.isShowingInfoModal = false; + window.setConfigHasSeenInfoModal(true); + }, + getRegionFrequencyRange: function(regionName) { + return window.getRegionFrequencyRange(regionName); + }, + showNodePositionHistory: function(nodeId) { + + // find node + const node = findNodeById(nodeId); + if(!node){ + return; + } + + // update ui + this.selectedNode = null; + this.selectedNodeToShowPositionHistory = node; + this.isPositionHistoryModalExpanded = true; + + // close node info tooltip as position history shows under it + window.closeAllTooltips(); + + // reset default time range when opening position history ui + // YYYY-MM-DDTHH:mm is the format expected by the datetime-local input type + this.positionHistoryDateTimeFrom = moment().subtract(1, "hours").format('YYYY-MM-DDTHH:mm'); + this.positionHistoryDateTimeTo = moment().format('YYYY-MM-DDTHH:mm'); + + // load position history + this.loadNodePositionHistory(nodeId); + + }, + onPositionHistoryQuickRangeClick: function(range) { + + // update position history time range + switch(range){ + case "1h": { + this.positionHistoryDateTimeFrom = moment().subtract(1, "hours").format('YYYY-MM-DDTHH:mm'); + this.positionHistoryDateTimeTo = moment().format('YYYY-MM-DDTHH:mm'); + break; + } + case "24h": { + this.positionHistoryDateTimeFrom = moment().subtract(24, "hours").format('YYYY-MM-DDTHH:mm'); + this.positionHistoryDateTimeTo = moment().format('YYYY-MM-DDTHH:mm'); + break; + } + case "7d": { + this.positionHistoryDateTimeFrom = moment().subtract(7, "days").format('YYYY-MM-DDTHH:mm'); + this.positionHistoryDateTimeTo = moment().format('YYYY-MM-DDTHH:mm'); + break; + } + } + + // reload position history + const node = this.selectedNodeToShowPositionHistory; + if(node){ + this.loadNodePositionHistory(node.node_id); + } + + }, + getShareLinkForNode: function(nodeId) { + return window.location.origin + `/?node_id=${nodeId}`; + }, + copyShareLinkForNode: function(nodeId) { + + // make sure copy to clipboard is supported + if(!navigator.clipboard || !navigator.clipboard.writeText){ + alert("Clipboard not supported. Site must be served via https on iOS."); + return; + } + + // copy share link to clipboard + const url = this.getShareLinkForNode(nodeId); + navigator.clipboard.writeText(url); + + // tell user we copied it + alert("Link copied to clipboard!"); + + }, + dismissShowingNodeConnections: function() { + window._onHideNodeConnectionsClick(); + this.selectedNodeToShowConnections = null; + }, + dismissShowingNodePositionHistory: function() { + this.selectedNodePositionHistory = []; + this.selectedNodeToShowPositionHistory = null; + this.selectedNodePositionHistoryMarkers = []; + this.selectedNodePositionHistoryPolyLines = []; + cleanUpPositionHistory(); + }, + formatUptimeSeconds: function(secondsToFormat) { + secondsToFormat = Number(secondsToFormat); + var days = Math.floor(secondsToFormat / (3600 * 24)); + var hours = Math.floor((secondsToFormat % (3600 * 24)) / 3600); + var minutes = Math.floor((secondsToFormat % 3600) / 60); + var seconds = Math.floor(secondsToFormat % 60); + var daysPlural = days === 1 ? 'day' : 'days'; + return `${days} ${daysPlural} ${hours}h ${minutes}m ${seconds}s`; + }, + formatTemperature: function(celsius) { + switch(this.configTemperatureFormat){ + case "celsius": { + return `${Number(celsius).toFixed(0)}°C`; + } + case "fahrenheit": { + const fahrenheit = this.celsiusToFahrenheit(celsius); + return `${fahrenheit.toFixed(0)}°F`; + } + } + }, + convertTemperature: function(celsius) { + switch(this.configTemperatureFormat){ + case "celsius": { + return celsius; + } + case "fahrenheit": { + return this.celsiusToFahrenheit(celsius); + } + } + }, + getTemperatureUnit: function() { + switch(this.configTemperatureFormat){ + case "celsius": return "°C"; + case "fahrenheit": return "°F"; + } + }, + celsiusToFahrenheit: function(celsius) { + return (celsius * 9/5) + 32; + }, + getNodeColour(nodeId) { + // convert node id to a hex colour + return "#" + (nodeId & 0x00FFFFFF).toString(16).padStart(6, '0'); + }, + getNodeTextColour(nodeId) { + + // extract rgb components + const r = (nodeId & 0xFF0000) >> 16; + const g = (nodeId & 0x00FF00) >> 8; + const b = nodeId & 0x0000FF; + + // calculate brightness + const brightness = ((r * 0.299) + (g * 0.587) + (b * 0.114)) / 255; + + // determine text color based on brightness + return brightness > 0.5 ? "#000000" : "#FFFFFF"; + + }, + }, + computed: { + searchedNodes() { + + // search nodes + const nodes = this.nodes.filter((node) => { + const matchesId = node.node_id?.toLowerCase()?.includes(this.searchText.toLowerCase()); + const matchesHexId = node.node_id_hex?.toLowerCase()?.includes(this.searchText.toLowerCase()); + const matchesLongName = node.long_name?.toLowerCase()?.includes(this.searchText.toLowerCase()); + const matchesShortName = node.short_name?.toLowerCase()?.includes(this.searchText.toLowerCase()); + return matchesId || matchesHexId || matchesLongName || matchesShortName; + }); + + // order alphabetically by long name + nodes.sort((nodeA, nodeB) => { + const nodeALongName = nodeA.long_name || ""; + const nodeBLongName = nodeB.long_name || ""; + return nodeALongName.localeCompare(nodeBLongName); + }); + + // only return the first 500 results to avoid ui lag... + return nodes.slice(0, 500); + + }, + selectedNodeLatestPowerMetric() { + const [ latestPowerMetric ] = this.selectedNodePowerMetrics.slice(-1); + return latestPowerMetric; + }, + }, + watch: { + configNodesMaxAgeInSeconds() { + window.setConfigNodesMaxAgeInSeconds(this.configNodesMaxAgeInSeconds); + }, + configNodesOfflineAgeInSeconds() { + window.setConfigNodesOfflineAgeInSeconds(this.configNodesOfflineAgeInSeconds); + }, + configWaypointsMaxAgeInSeconds() { + window.setConfigWaypointsMaxAgeInSeconds(this.configWaypointsMaxAgeInSeconds); + }, + configConnectionsMaxDistanceInMeters() { + window.setConfigConnectionsMaxDistanceInMeters(this.configConnectionsMaxDistanceInMeters); + }, + configZoomLevelGoToNode() { + window.setConfigZoomLevelGoToNode(this.configZoomLevelGoToNode); + }, + configAutoUpdatePositionInUrl() { + window.setConfigAutoUpdatePositionInUrl(this.configAutoUpdatePositionInUrl); + }, + configEnableMapAnimations() { + window.setConfigEnableMapAnimations(this.configEnableMapAnimations); + }, + configTemperatureFormat() { + window.setConfigTemperatureFormat(this.configTemperatureFormat); + }, + configConnectionsTimePeriodInSeconds() { + window.setConfigConnectionsTimePeriodInSeconds(this.configConnectionsTimePeriodInSeconds); + }, + configConnectionsColoredLines() { + window.setConfigConnectionsColoredLines(this.configConnectionsColoredLines); + }, + configConnectionsBidirectionalOnly() { + window.setConfigConnectionsBidirectionalOnly(this.configConnectionsBidirectionalOnly); + }, + configConnectionsMinSnrDb() { + window.setConfigConnectionsMinSnrDb(this.configConnectionsMinSnrDb); + }, + configConnectionsBidirectionalMinSnr() { + window.setConfigConnectionsBidirectionalMinSnr(this.configConnectionsBidirectionalMinSnr); + }, + deviceMetricsTimeRange() { + this.loadNodeDeviceMetrics(this.selectedNode.node_id); + }, + environmentMetricsTimeRange() { + this.loadNodeEnvironmentMetrics(this.selectedNode.node_id); + }, + powerMetricsTimeRange() { + this.loadNodePowerMetrics(this.selectedNode.node_id); + }, + }, +}).mount('#app'); \ No newline at end of file diff --git a/src/public/assets/js/config.js b/src/public/assets/js/config.js new file mode 100644 index 0000000..5866a4c --- /dev/null +++ b/src/public/assets/js/config.js @@ -0,0 +1,199 @@ +function getConfigHasSeenInfoModal() { + return localStorage.getItem("config_has_seen_info_modal") === "true"; +} + +function setConfigHasSeenInfoModal(value) { + return localStorage.setItem("config_has_seen_info_modal", value); +} + +function getConfigAutoUpdatePositionInUrl() { + // use user preference, or enable by default + const value = localStorage.getItem("config_auto_update_position_in_url"); + return value === "true" || value == null; +} + +function setConfigAutoUpdatePositionInUrl(value) { + return localStorage.setItem("config_auto_update_position_in_url", value); +} + +function getConfigEnableMapAnimations() { + + const value = localStorage.getItem("config_enable_map_animations"); + + // enable animations by default + if(value === null){ + return true; + } + + return value === "true"; + +} + +function setConfigEnableMapAnimations(value) { + return localStorage.setItem("config_enable_map_animations", value); +} + +function getConfigTemperatureFormat() { + return localStorage.getItem("config_temperature_format") || "celsius"; +} + +function setConfigTemperatureFormat(format) { + return localStorage.setItem("config_temperature_format", format); +} + +function getConfigMapSelectedTileLayer() { + return localStorage.getItem("config_map_selected_tile_layer") || "Thunderforest Neighbourhood"; +} + +function setConfigMapSelectedTileLayer(layer) { + return localStorage.setItem("config_map_selected_tile_layer", layer); +} + +function getConfigMapEnabledOverlayLayers() { + + try { + const value = localStorage.getItem("config_map_enabled_overlay_layers"); + if(value){ + return JSON.parse(value); + } + } catch(e) {} + + // overlays enabled by default + return ["Legend", "Position History", "Traceroutes"]; + +} + +function setConfigMapEnabledOverlayLayers(layers) { + return localStorage.setItem("config_map_enabled_overlay_layers", JSON.stringify(layers)); +} + +function getConfigNodesMaxAgeInSeconds() { + const value = localStorage.getItem("config_nodes_max_age_in_seconds"); + return value != null ? parseInt(value) : null; +} + +function setConfigNodesMaxAgeInSeconds(value) { + if(value != null){ + return localStorage.setItem("config_nodes_max_age_in_seconds", value); + } else { + return localStorage.removeItem("config_nodes_max_age_in_seconds"); + } +} + +function getConfigNodesOfflineAgeInSeconds() { + const value = localStorage.getItem("config_nodes_offline_age_in_seconds"); + return value != null ? parseInt(value) : 10800; +} + +function setConfigNodesOfflineAgeInSeconds(value) { + if(value != null){ + return localStorage.setItem("config_nodes_offline_age_in_seconds", value); + } else { + return localStorage.removeItem("config_nodes_offline_age_in_seconds"); + } +} + +function getConfigWaypointsMaxAgeInSeconds() { + const value = localStorage.getItem("config_waypoints_max_age_in_seconds"); + return value != null ? parseInt(value) : null; +} + +function setConfigWaypointsMaxAgeInSeconds(value) { + if(value != null){ + return localStorage.setItem("config_waypoints_max_age_in_seconds", value); + } else { + return localStorage.removeItem("config_waypoints_max_age_in_seconds"); + } +} + +function getConfigConnectionsMaxDistanceInMeters() { + const value = localStorage.getItem("config_connections_max_distance_in_meters"); + // default to 70km (70,000 meters) + return value != null ? parseInt(value) : 70000; +} + +function setConfigConnectionsMaxDistanceInMeters(value) { + return localStorage.setItem("config_connections_max_distance_in_meters", value); +} + +function getConfigZoomLevelGoToNode() { + const value = localStorage.getItem("config_zoom_level_go_to_node"); + const parsedValue = value != null ? parseInt(value) : null; + return parsedValue || 15; +} + +function setConfigZoomLevelGoToNode(value) { + return localStorage.setItem("config_zoom_level_go_to_node", value); +} + +function getConfigConnectionsTimePeriodInSeconds() { + const value = localStorage.getItem("config_connections_time_period_in_seconds"); + // default to 7 days if unset + return value != null ? parseInt(value) : 604800; +} + +function setConfigConnectionsTimePeriodInSeconds(value) { + return localStorage.setItem("config_connections_time_period_in_seconds", value); +} + +function getConfigConnectionsColoredLines() { + const value = localStorage.getItem("config_connections_colored_lines"); + // disable colored lines by default + if(value === null){ + return false; + } + return value === "true"; +} + +function setConfigConnectionsColoredLines(value) { + return localStorage.setItem("config_connections_colored_lines", value); +} + +function getConfigConnectionsBidirectionalOnly() { + const value = localStorage.getItem("config_connections_bidirectional_only"); + // disable bidirectional filter by default + if(value === null){ + return false; + } + return value === "true"; +} + +function setConfigConnectionsBidirectionalOnly(value) { + return localStorage.setItem("config_connections_bidirectional_only", value); +} + +function getConfigConnectionsMinSnrDb() { + const value = localStorage.getItem("config_connections_min_snr_db"); + // default to null (unset) + if(value === null || value === ""){ + return null; + } + const parsed = parseFloat(value); + return isNaN(parsed) ? null : parsed; +} + +function setConfigConnectionsMinSnrDb(value) { + if(value === null || value === "" || value === undefined){ + return localStorage.removeItem("config_connections_min_snr_db"); + } + // Convert to string for localStorage (handles both number and string inputs) + const stringValue = typeof value === "number" ? value.toString() : String(value); + return localStorage.setItem("config_connections_min_snr_db", stringValue); +} + +function getConfigConnectionsBidirectionalMinSnr() { + const value = localStorage.getItem("config_connections_bidirectional_min_snr"); + // disable bidirectional minimum SNR by default + if(value === null){ + return false; + } + return value === "true"; +} + +function setConfigConnectionsBidirectionalMinSnr(value) { + return localStorage.setItem("config_connections_bidirectional_min_snr", value); +} + +function isMobile() { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); +} \ No newline at end of file diff --git a/src/public/assets/js/map.js b/src/public/assets/js/map.js new file mode 100644 index 0000000..e25c067 --- /dev/null +++ b/src/public/assets/js/map.js @@ -0,0 +1,1692 @@ +// global state +var nodes = []; +var nodeMarkers = {}; +var selectedNodeOutlineCircle = null; +var waypoints = []; + +// set map bounds to be a little more than full size to prevent panning off screen +var bounds = [ + [-100, 70], // top left + [100, 500], // bottom right +]; + +// create map positioned over NRW +if(!isMobile()){ + var map = L.map('map', { + maxBounds: bounds, + }).setView([ + 51.1, + 366.82, + ], 9); +} else { + var map = L.map('map', { + maxBounds: bounds, + }).setView([ + 51.1, + 366.82, + ], 8); +} + +// remove leaflet link +map.attributionControl.setPrefix(''); + +var openThunderforestLandscapeMapTileLayer = L.tileLayer('https://tiles.nixware.dev/landscape/{z}/{x}/{y}.png', { + maxZoom: 22, + attribution: 'Tiles © Gravitystorm Limited | Data from Meshtastic', +}); + +var openThunderforestAtlasMapTileLayer = L.tileLayer('https://tiles.nixware.dev/atlas/{z}/{x}/{y}.png', { + maxZoom: 22, + attribution: 'Tiles © Gravitystorm Limited | Data from Meshtastic', +}); + +var openThunderforestNeighbourhoodMapTileLayer = L.tileLayer('https://tiles.nixware.dev/neighbourhood/{z}/{x}/{y}.png', { + maxZoom: 22, + attribution: 'Tiles © Gravitystorm Limited | Data from Meshtastic', +}); + +var openStreetMapTileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 22, // increase from 18 to 22 + attribution: 'Tiles © OpenStreetMap | Data from Meshtastic', +}); + +var openTopoMapTileLayer = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { + maxZoom: 17, // open topo map doesn't have tiles closer than this + attribution: 'Tiles © OpenStreetMap | Data from Meshtastic', +}); + +var esriWorldImageryTileLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { + maxZoom: 21, // esri doesn't have tiles closer than this + attribution: 'Tiles © Esri | Data from Meshtastic' +}); + +var googleSatelliteTileLayer = L.tileLayer('https://{s}.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', { + maxZoom: 21, + subdomains: ['mt0', 'mt1', 'mt2', 'mt3'], + attribution: 'Tiles © Google | Data from Meshtastic' +}); + +var googleHybridTileLayer = L.tileLayer('https://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', { + maxZoom: 21, + subdomains: ['mt0', 'mt1', 'mt2', 'mt3'], + attribution: 'Tiles © Google | Data from Meshtastic' +}); + +var tileLayers = { + "Thunderforest Neighbourhood": openThunderforestNeighbourhoodMapTileLayer, + "Thunderforest Landscape": openThunderforestLandscapeMapTileLayer, + "Thunderforest Atlas": openThunderforestAtlasMapTileLayer, + "OpenStreetMap": openStreetMapTileLayer, + "OpenTopoMap": openTopoMapTileLayer, + "Esri Satellite": esriWorldImageryTileLayer, + "Google Satellite": googleSatelliteTileLayer, + "Google Hybrid": googleHybridTileLayer, +}; + +// use tile layer based on config +const selectedTileLayerName = getConfigMapSelectedTileLayer(); +const selectedTileLayer = tileLayers[selectedTileLayerName] || openThunderforestNeighbourhoodMapTileLayer; +selectedTileLayer.addTo(map); + +// create layer groups +var nodesLayerGroup = new L.LayerGroup(); +var backboneConnectionsLayerGroup = new L.LayerGroup(); +var nodeConnectionsLayerGroup = new L.LayerGroup(); +var nodesClusteredLayerGroup = L.markerClusterGroup({ + showCoverageOnHover: false, + disableClusteringAtZoom: 10, // zoom level where node clustering is disabled +}); +var nodesRouterLayerGroup = L.markerClusterGroup({ + showCoverageOnHover: false, + disableClusteringAtZoom: 10, // zoom level where node clustering is disabled +}); +var nodesBackboneLayerGroup = new L.LayerGroup(); +//var nodesMediumFastLayerGroup = new L.LayerGroup(); +var nodesShortSlowLayerGroup = new L.LayerGroup(); +var nodesLongFastLayerGroup = new L.LayerGroup(); +var waypointsLayerGroup = new L.LayerGroup(); +var nodePositionHistoryLayerGroup = new L.LayerGroup(); +var traceroutesLayerGroup = new L.LayerGroup(); +var connectionsLayerGroup = new L.LayerGroup(); + +// create icons +var iconMqttConnected = L.divIcon({ + className: 'icon-mqtt-connected', + iconSize: [16, 16], // increase from 12px to 16px to make hover easier +}); + +var iconLongFast = L.divIcon({ + className: 'icon-longfast', + iconSize: [16, 16], // increase from 12px to 16px to make hover easier +}); + +/*var iconMediumFast = L.divIcon({ + className: 'icon-mediumfast', + iconSize: [16, 16], // increase from 12px to 16px to make hover easier +});*/ + +var iconShortSlow = L.divIcon({ + className: 'icon-shortslow', + iconSize: [16, 16], +}); + +var iconMqttDisconnected = L.divIcon({ + className: 'icon-mqtt-disconnected', + iconSize: [16, 16], // increase from 12px to 16px to make hover easier +}); + +var iconOffline = L.divIcon({ + className: 'icon-offline', + iconSize: [16, 16], // increase from 12px to 16px to make hover easier +}); + +var iconPositionHistory = L.divIcon({ + className: 'icon-position-history', + iconSize: [16, 16], // increase from 12px to 16px to make hover easier +}); + +var iconTracerouteStart = L.divIcon({ + className: 'icon-traceroute-start', + iconSize: [16, 16], +}); + +var iconTracerouteEnd = L.divIcon({ + className: 'icon-traceroute-end', + iconSize: [16, 16], +}); + +// create legend +var legendLayerGroup = new L.LayerGroup(); +var legend = L.control({position: 'bottomleft'}); +legend.onAdd = function (map) { + var div = L.DomUtil.create('div', 'leaflet-control-layers'); + div.style.backgroundColor = 'white'; + div.style.padding = '12px'; + div.innerHTML = `
` +
+ `${escapeString(node.long_name)}` +
+ `