diff --git a/Dockerfile b/Dockerfile index 4094a66..ecb1766 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,15 @@ -FROM node:lts-alpine AS build - -RUN apk add --no-cache openssl +FROM node:lts-alpine WORKDIR /app +RUN apk add --no-cache openssl + # 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 --mount=type=cache,target=/root/.npm npm ci --omit=dev +RUN npm ci -# Copy the rest of your source +# Copy the rest of the source COPY . . -# 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 +EXPOSE 8080 \ No newline at end of file diff --git a/README.md b/README.md index 2ee653b..854146a 100644 --- a/README.md +++ b/README.md @@ -65,11 +65,6 @@ 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 026bb0c..85d17e6 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -30,18 +30,6 @@ 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 deleted file mode 100755 index 71d4048..0000000 --- a/docker/ws.sh +++ /dev/null @@ -1,5 +0,0 @@ -#!/bin/sh - -echo "Starting websocket publisher" -exec node src/ws.js ${WS_OPTS} - diff --git a/package-lock.json b/package-lock.json index 1fb2318..698e702 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,30 +14,15 @@ "command-line-usage": "^7.0.3", "compression": "^1.8.1", "cors": "^2.8.5", - "express": "^5.2.1", + "express": "^5.0.0", "mqtt": "^5.14.1", - "protobufjs": "^7.5.4", - "ws": "^8.18.3" + "protobufjs": "^7.5.4" }, "devDependencies": { - "jest": "^30.1.3", + "jest": "^30.2.0", "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", @@ -54,9 +39,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "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==", "dev": true, "license": "MIT", "engines": { @@ -64,23 +49,22 @@ } }, "node_modules/@babel/core": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", - "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "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==", "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.3", - "@babel/parser": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2", + "@babel/traverse": "^7.28.4", + "@babel/types": "^7.28.4", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -96,9 +80,9 @@ } }, "node_modules/@babel/core/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==", "dev": true, "license": "MIT", "dependencies": { @@ -247,27 +231,27 @@ } }, "node_modules/@babel/helpers": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", - "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, "license": "MIT", "dependencies": { "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", - "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", + "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.28.2" + "@babel/types": "^7.28.4" }, "bin": { "parser": "bin/babel-parser.js" @@ -540,18 +524,18 @@ } }, "node_modules/@babel/traverse": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", - "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", + "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", "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.3", + "@babel/parser": "^7.28.4", "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2", + "@babel/types": "^7.28.4", "debug": "^4.3.1" }, "engines": { @@ -559,9 +543,9 @@ } }, "node_modules/@babel/traverse/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==", "dev": true, "license": "MIT", "dependencies": { @@ -584,9 +568,9 @@ "license": "MIT" }, "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", + "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", "dev": true, "license": "MIT", "dependencies": { @@ -684,17 +668,17 @@ } }, "node_modules/@jest/console": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.1.2.tgz", - "integrity": "sha512-BGMAxj8VRmoD0MoA/jo9alMXSRoqW8KPeqOfEo1ncxnRLatTBCpRoOwlwlEMdudp68Q6WSGwYrrLtTGOh8fLzw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", + "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", - "jest-message-util": "30.1.0", - "jest-util": "30.0.5", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", "slash": "^3.0.0" }, "engines": { @@ -702,39 +686,39 @@ } }, "node_modules/@jest/core": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.1.3.tgz", - "integrity": "sha512-LIQz7NEDDO1+eyOA2ZmkiAyYvZuo6s1UxD/e2IHldR6D7UYogVq3arTmli07MkENLq6/3JEQjp0mA8rrHHJ8KQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", + "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.1.2", + "@jest/console": "30.2.0", "@jest/pattern": "30.0.1", - "@jest/reporters": "30.1.3", - "@jest/test-result": "30.1.3", - "@jest/transform": "30.1.2", - "@jest/types": "30.0.5", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", "@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.0.5", - "jest-config": "30.1.3", - "jest-haste-map": "30.1.0", - "jest-message-util": "30.1.0", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", "jest-regex-util": "30.0.1", - "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", + "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", "micromatch": "^4.0.8", - "pretty-format": "30.0.5", + "pretty-format": "30.2.0", "slash": "^3.0.0" }, "engines": { @@ -760,39 +744,39 @@ } }, "node_modules/@jest/environment": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.1.2.tgz", - "integrity": "sha512-N8t1Ytw4/mr9uN28OnVf0SYE2dGhaIxOVYcwsf9IInBKjvofAjbFRvedvBBlyTYk2knbJTiEjEJ2PyyDIBnd9w==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", "dev": true, "license": "MIT", "dependencies": { - "@jest/fake-timers": "30.1.2", - "@jest/types": "30.0.5", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", - "jest-mock": "30.0.5" + "jest-mock": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.1.2.tgz", - "integrity": "sha512-tyaIExOwQRCxPCGNC05lIjWJztDwk2gPDNSDGg1zitXJJ8dC3++G/CRjE5mb2wQsf89+lsgAgqxxNpDLiCViTA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", "dev": true, "license": "MIT", "dependencies": { - "expect": "30.1.2", - "jest-snapshot": "30.1.2" + "expect": "30.2.0", + "jest-snapshot": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/@jest/expect-utils": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.1.2.tgz", - "integrity": "sha512-HXy1qT/bfdjCv7iC336ExbqqYtZvljrV8odNdso7dWK9bSeHtLlvwWWC3YSybSPL03Gg5rug6WLCZAZFH72m0A==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", "dev": true, "license": "MIT", "dependencies": { @@ -803,18 +787,18 @@ } }, "node_modules/@jest/fake-timers": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.1.2.tgz", - "integrity": "sha512-Beljfv9AYkr9K+ETX9tvV61rJTY706BhBUtiaepQHeEGfe0DbpvUA5Z3fomwc5Xkhns6NWrcFDZn+72fLieUnA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@sinonjs/fake-timers": "^13.0.0", "@types/node": "*", - "jest-message-util": "30.1.0", - "jest-mock": "30.0.5", - "jest-util": "30.0.5" + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -831,16 +815,16 @@ } }, "node_modules/@jest/globals": { - "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==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", + "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.1.2", - "@jest/expect": "30.1.2", - "@jest/types": "30.0.5", - "jest-mock": "30.0.5" + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -861,17 +845,17 @@ } }, "node_modules/@jest/reporters": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.1.3.tgz", - "integrity": "sha512-VWEQmJWfXMOrzdFEOyGjUEOuVXllgZsoPtEHZzfdNz18RmzJ5nlR6kp8hDdY8dDS1yGOXAY7DHT+AOHIPSBV0w==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", + "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "30.1.2", - "@jest/test-result": "30.1.3", - "@jest/transform": "30.1.2", - "@jest/types": "30.0.5", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", "@jridgewell/trace-mapping": "^0.3.25", "@types/node": "*", "chalk": "^4.1.2", @@ -884,9 +868,9 @@ "istanbul-lib-report": "^3.0.0", "istanbul-lib-source-maps": "^5.0.0", "istanbul-reports": "^3.1.3", - "jest-message-util": "30.1.0", - "jest-util": "30.0.5", - "jest-worker": "30.1.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", "slash": "^3.0.0", "string-length": "^4.0.2", "v8-to-istanbul": "^9.0.1" @@ -917,13 +901,13 @@ } }, "node_modules/@jest/snapshot-utils": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.1.2.tgz", - "integrity": "sha512-vHoMTpimcPSR7OxS2S0V1Cpg8eKDRxucHjoWl5u4RQcnxqQrV3avETiFpl8etn4dqxEGarBeHbIBety/f8mLXw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", + "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "natural-compare": "^1.4.0" @@ -948,14 +932,14 @@ } }, "node_modules/@jest/test-result": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.1.3.tgz", - "integrity": "sha512-P9IV8T24D43cNRANPPokn7tZh0FAFnYS2HIfi5vK18CjRkTDR9Y3e1BoEcAJnl4ghZZF4Ecda4M/k41QkvurEQ==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@jest/console": "30.1.2", - "@jest/types": "30.0.5", + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", "@types/istanbul-lib-coverage": "^2.0.6", "collect-v8-coverage": "^1.0.2" }, @@ -964,15 +948,15 @@ } }, "node_modules/@jest/test-sequencer": { - "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==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.1.3", + "@jest/test-result": "30.2.0", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.1.0", + "jest-haste-map": "30.2.0", "slash": "^3.0.0" }, "engines": { @@ -980,23 +964,23 @@ } }, "node_modules/@jest/transform": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.1.2.tgz", - "integrity": "sha512-UYYFGifSgfjujf1Cbd3iU/IQoSd6uwsj8XHj5DSDf5ERDcWMdJOPTkHWXj4U+Z/uMagyOQZ6Vne8C4nRIrCxqA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", "dev": true, "license": "MIT", "dependencies": { "@babel/core": "^7.27.4", - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@jridgewell/trace-mapping": "^0.3.25", - "babel-plugin-istanbul": "^7.0.0", + "babel-plugin-istanbul": "^7.0.1", "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.1.0", + "jest-haste-map": "30.2.0", "jest-regex-util": "30.0.1", - "jest-util": "30.0.5", + "jest-util": "30.2.0", "micromatch": "^4.0.8", "pirates": "^4.0.7", "slash": "^3.0.0", @@ -1007,9 +991,9 @@ } }, "node_modules/@jest/types": { - "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==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", "dev": true, "license": "MIT", "dependencies": { @@ -1036,6 +1020,17 @@ "@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", @@ -1054,9 +1049,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -1275,9 +1270,9 @@ "license": "MIT" }, "node_modules/@tybys/wasm-util": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", - "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, "license": "MIT", "optional": true, @@ -1712,9 +1707,9 @@ } }, "node_modules/ansi-regex": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "dev": true, "license": "MIT", "engines": { @@ -1772,16 +1767,16 @@ } }, "node_modules/babel-jest": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.1.2.tgz", - "integrity": "sha512-IQCus1rt9kaSh7PQxLYRY5NmkNrNlU2TpabzwV7T2jljnpdHOcmnYYv8QmE04Li4S3a2Lj8/yXyET5pBarPr6g==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/transform": "30.1.2", + "@jest/transform": "30.2.0", "@types/babel__core": "^7.20.5", - "babel-plugin-istanbul": "^7.0.0", - "babel-preset-jest": "30.0.1", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "slash": "^3.0.0" @@ -1790,15 +1785,18 @@ "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.11.0" + "@babel/core": "^7.11.0 || ^8.0.0-0" } }, "node_modules/babel-plugin-istanbul": { - "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==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", "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", @@ -1811,14 +1809,12 @@ } }, "node_modules/babel-plugin-jest-hoist": { - "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==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.27.3", "@types/babel__core": "^7.20.5" }, "engines": { @@ -1853,20 +1849,20 @@ } }, "node_modules/babel-preset-jest": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", - "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", "dev": true, "license": "MIT", "dependencies": { - "babel-plugin-jest-hoist": "30.0.1", - "babel-preset-current-node-syntax": "^1.1.0" + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" }, "peerDependencies": { - "@babel/core": "^7.11.0" + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" } }, "node_modules/balanced-match": { @@ -1896,6 +1892,16 @@ ], "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", @@ -1909,34 +1915,28 @@ } }, "node_modules/body-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", - "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", - "license": "MIT", + "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==", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.3", + "debug": "^4.4.0", "http-errors": "^2.0.0", - "iconv-lite": "^0.7.0", + "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.1", - "type-is": "^2.0.1" + "raw-body": "^3.0.0", + "type-is": "^2.0.0" }, "engines": { "node": ">=18" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" } }, "node_modules/body-parser/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dependencies": { "ms": "^2.1.3" }, @@ -1952,8 +1952,7 @@ "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==", - "license": "MIT" + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, "node_modules/brace-expansion": { "version": "2.0.2", @@ -1991,9 +1990,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", - "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", + "version": "4.26.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", + "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", "dev": true, "funding": [ { @@ -2010,11 +2009,11 @@ } ], "license": "MIT", - "peer": true, "dependencies": { - "caniuse-lite": "^1.0.30001737", - "electron-to-chromium": "^1.5.211", - "node-releases": "^2.0.19", + "baseline-browser-mapping": "^2.8.3", + "caniuse-lite": "^1.0.30001741", + "electron-to-chromium": "^1.5.218", + "node-releases": "^2.0.21", "update-browserslist-db": "^1.1.3" }, "bin": { @@ -2105,7 +2104,6 @@ "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" @@ -2118,7 +2116,6 @@ "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" @@ -2151,9 +2148,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001739", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", - "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", + "version": "1.0.30001745", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001745.tgz", + "integrity": "sha512-ywt6i8FzvdgrrrGbr1jZVObnVv6adj+0if2/omv9cmR2oiZs30zL4DIyaptKcbOrBdOIc74QTMoJvSE2QHh5UQ==", "dev": true, "funding": [ { @@ -2512,7 +2509,6 @@ "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" } @@ -2576,9 +2572,9 @@ } }, "node_modules/dedent": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", - "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.0.tgz", + "integrity": "sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2659,7 +2655,6 @@ "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", @@ -2693,9 +2688,9 @@ } }, "node_modules/electron-to-chromium": { - "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==", + "version": "1.5.227", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.227.tgz", + "integrity": "sha512-ITxuoPfJu3lsNWUi2lBM2PaBPYgH3uqmxut5vmBxgYvyI4AlJ6P3Cai1O76mOrkJCBzq0IxWg/NtqOrpu/0gKA==", "dev": true, "license": "ISC" }, @@ -2738,9 +2733,9 @@ } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "dev": true, "license": "MIT", "dependencies": { @@ -2751,7 +2746,6 @@ "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" } @@ -2760,7 +2754,6 @@ "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" } @@ -2769,7 +2762,6 @@ "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" }, @@ -2884,37 +2876,35 @@ } }, "node_modules/expect": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-30.1.2.tgz", - "integrity": "sha512-xvHszRavo28ejws8FpemjhwswGj4w/BetHIL8cU49u4sGyXDw2+p3YbeDbj6xzlxi6kWTjIRSTJ+9sNXPnF0Zg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/expect-utils": "30.1.2", + "@jest/expect-utils": "30.2.0", "@jest/get-type": "30.1.0", - "jest-matcher-utils": "30.1.2", - "jest-message-util": "30.1.0", - "jest-mock": "30.0.5", - "jest-util": "30.0.5" + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/express": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", - "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", - "license": "MIT", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", "dependencies": { "accepts": "^2.0.0", - "body-parser": "^2.2.1", + "body-parser": "^2.2.0", "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", @@ -2972,6 +2962,25 @@ } } }, + "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", @@ -3202,7 +3211,6 @@ "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" } @@ -3231,7 +3239,6 @@ "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", @@ -3265,7 +3272,6 @@ "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" @@ -3330,7 +3336,6 @@ "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" }, @@ -3357,7 +3362,6 @@ "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" }, @@ -3369,7 +3373,6 @@ "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" }, @@ -3391,23 +3394,26 @@ "license": "MIT" }, "node_modules/http-errors": { - "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", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", "dependencies": { - "depd": "~2.0.0", - "inherits": "~2.0.4", - "setprototypeof": "~1.2.0", - "statuses": "~2.0.2", - "toidentifier": "~1.0.1" + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" }, "engines": { "node": ">= 0.8" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" + } + }, + "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" } }, "node_modules/human-signals": { @@ -3421,19 +3427,14 @@ } }, "node_modules/iconv-lite": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", - "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", - "license": "MIT", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", "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": { @@ -3663,9 +3664,9 @@ } }, "node_modules/istanbul-lib-source-maps/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==", "dev": true, "license": "MIT", "dependencies": { @@ -3718,16 +3719,16 @@ } }, "node_modules/jest": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest/-/jest-30.1.3.tgz", - "integrity": "sha512-Ry+p2+NLk6u8Agh5yVqELfUJvRfV51hhVBRIB5yZPY7mU0DGBmOuFG5GebZbMbm86cdQNK0fhJuDX8/1YorISQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", + "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.1.3", - "@jest/types": "30.0.5", + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", "import-local": "^3.2.0", - "jest-cli": "30.1.3" + "jest-cli": "30.2.0" }, "bin": { "jest": "bin/jest.js" @@ -3745,14 +3746,14 @@ } }, "node_modules/jest-changed-files": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.5.tgz", - "integrity": "sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", + "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", "dev": true, "license": "MIT", "dependencies": { "execa": "^5.1.1", - "jest-util": "30.0.5", + "jest-util": "30.2.0", "p-limit": "^3.1.0" }, "engines": { @@ -3760,29 +3761,29 @@ } }, "node_modules/jest-circus": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.1.3.tgz", - "integrity": "sha512-Yf3dnhRON2GJT4RYzM89t/EXIWNxKTpWTL9BfF3+geFetWP4XSvJjiU1vrWplOiUkmq8cHLiwuhz+XuUp9DscA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", + "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.1.2", - "@jest/expect": "30.1.2", - "@jest/test-result": "30.1.3", - "@jest/types": "30.0.5", + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "co": "^4.6.0", "dedent": "^1.6.0", "is-generator-fn": "^2.1.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", + "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", "p-limit": "^3.1.0", - "pretty-format": "30.0.5", + "pretty-format": "30.2.0", "pure-rand": "^7.0.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" @@ -3792,21 +3793,21 @@ } }, "node_modules/jest-cli": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.1.3.tgz", - "integrity": "sha512-G8E2Ol3OKch1DEeIBl41NP7OiC6LBhfg25Btv+idcusmoUSpqUkbrneMqbW9lVpI/rCKb/uETidb7DNteheuAQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", + "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/core": "30.1.3", - "@jest/test-result": "30.1.3", - "@jest/types": "30.0.5", + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", "chalk": "^4.1.2", "exit-x": "^0.2.2", "import-local": "^3.2.0", - "jest-config": "30.1.3", - "jest-util": "30.0.5", - "jest-validate": "30.1.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", "yargs": "^17.7.2" }, "bin": { @@ -3825,34 +3826,34 @@ } }, "node_modules/jest-config": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.1.3.tgz", - "integrity": "sha512-M/f7gqdQEPgZNA181Myz+GXCe8jXcJsGjCMXUzRj22FIXsZOyHNte84e0exntOvdPaeh9tA0w+B8qlP2fAezfw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", "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.1.3", - "@jest/types": "30.0.5", - "babel-jest": "30.1.2", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", "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.1.3", - "jest-docblock": "30.0.1", - "jest-environment-node": "30.1.2", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.1.3", - "jest-runner": "30.1.3", - "jest-util": "30.0.5", - "jest-validate": "30.1.0", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", "micromatch": "^4.0.8", "parse-json": "^5.2.0", - "pretty-format": "30.0.5", + "pretty-format": "30.2.0", "slash": "^3.0.0", "strip-json-comments": "^3.1.1" }, @@ -3877,25 +3878,25 @@ } }, "node_modules/jest-diff": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.2.tgz", - "integrity": "sha512-4+prq+9J61mOVXCa4Qp8ZjavdxzrWQXrI80GNxP8f4tkI2syPuPrJgdRPZRrfUTRvIoUwcmNLbqEJy9W800+NQ==", + "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==", "dev": true, "license": "MIT", "dependencies": { "@jest/diff-sequences": "30.0.1", "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "pretty-format": "30.0.5" + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-docblock": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz", - "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", "dev": true, "license": "MIT", "dependencies": { @@ -3906,56 +3907,56 @@ } }, "node_modules/jest-each": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.1.0.tgz", - "integrity": "sha512-A+9FKzxPluqogNahpCv04UJvcZ9B3HamqpDNWNKDjtxVRYB8xbZLFuCr8JAJFpNp83CA0anGQFlpQna9Me+/tQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", + "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "chalk": "^4.1.2", - "jest-util": "30.0.5", - "pretty-format": "30.0.5" + "jest-util": "30.2.0", + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-environment-node": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.1.2.tgz", - "integrity": "sha512-w8qBiXtqGWJ9xpJIA98M0EIoq079GOQRQUyse5qg1plShUCQ0Ek1VTTcczqKrn3f24TFAgFtT+4q3aOXvjbsuA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", + "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.1.2", - "@jest/fake-timers": "30.1.2", - "@jest/types": "30.0.5", + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", - "jest-mock": "30.0.5", - "jest-util": "30.0.5", - "jest-validate": "30.1.0" + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-haste-map": { - "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==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@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.0.5", - "jest-worker": "30.1.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", "micromatch": "^4.0.8", "walker": "^1.0.8" }, @@ -3967,49 +3968,49 @@ } }, "node_modules/jest-leak-detector": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.1.0.tgz", - "integrity": "sha512-AoFvJzwxK+4KohH60vRuHaqXfWmeBATFZpzpmzNmYTtmRMiyGPVhkXpBqxUQunw+dQB48bDf4NpUs6ivVbRv1g==", + "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==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "pretty-format": "30.0.5" + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-matcher-utils": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.1.2.tgz", - "integrity": "sha512-7ai16hy4rSbDjvPTuUhuV8nyPBd6EX34HkBsBcBX2lENCuAQ0qKCPb/+lt8OSWUa9WWmGYLy41PrEzkwRwoGZQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", "chalk": "^4.1.2", - "jest-diff": "30.1.2", - "pretty-format": "30.0.5" + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-message-util": { - "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==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", "dev": true, "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/stack-utils": "^2.0.3", "chalk": "^4.1.2", "graceful-fs": "^4.2.11", "micromatch": "^4.0.8", - "pretty-format": "30.0.5", + "pretty-format": "30.2.0", "slash": "^3.0.0", "stack-utils": "^2.0.6" }, @@ -4018,15 +4019,15 @@ } }, "node_modules/jest-mock": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", - "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/node": "*", - "jest-util": "30.0.5" + "jest-util": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -4061,18 +4062,18 @@ } }, "node_modules/jest-resolve": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.1.3.tgz", - "integrity": "sha512-DI4PtTqzw9GwELFS41sdMK32Ajp3XZQ8iygeDMWkxlRhm7uUTOFSZFVZABFuxr0jvspn8MAYy54NxZCsuCTSOw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", + "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", "dev": true, "license": "MIT", "dependencies": { "chalk": "^4.1.2", "graceful-fs": "^4.2.11", - "jest-haste-map": "30.1.0", + "jest-haste-map": "30.2.0", "jest-pnp-resolver": "^1.2.3", - "jest-util": "30.0.5", - "jest-validate": "30.1.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", "slash": "^3.0.0", "unrs-resolver": "^1.7.11" }, @@ -4081,46 +4082,46 @@ } }, "node_modules/jest-resolve-dependencies": { - "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==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", + "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", "dev": true, "license": "MIT", "dependencies": { "jest-regex-util": "30.0.1", - "jest-snapshot": "30.1.2" + "jest-snapshot": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, "node_modules/jest-runner": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.1.3.tgz", - "integrity": "sha512-dd1ORcxQraW44Uz029TtXj85W11yvLpDuIzNOlofrC8GN+SgDlgY4BvyxJiVeuabA1t6idjNbX59jLd2oplOGQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", + "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", "dev": true, "license": "MIT", "dependencies": { - "@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", + "@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", "@types/node": "*", "chalk": "^4.1.2", "emittery": "^0.13.1", "exit-x": "^0.2.2", "graceful-fs": "^4.2.11", - "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", + "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", "p-limit": "^3.1.0", "source-map-support": "0.5.13" }, @@ -4129,32 +4130,32 @@ } }, "node_modules/jest-runtime": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.1.3.tgz", - "integrity": "sha512-WS8xgjuNSphdIGnleQcJ3AKE4tBKOVP+tKhCD0u+Tb2sBmsU8DxfbBpZX7//+XOz81zVs4eFpJQwBNji2Y07DA==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "30.1.2", - "@jest/fake-timers": "30.1.2", - "@jest/globals": "30.1.2", + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", "@jest/source-map": "30.0.1", - "@jest/test-result": "30.1.3", - "@jest/transform": "30.1.2", - "@jest/types": "30.0.5", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", "@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.1.0", - "jest-message-util": "30.1.0", - "jest-mock": "30.0.5", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", "jest-regex-util": "30.0.1", - "jest-resolve": "30.1.3", - "jest-snapshot": "30.1.2", - "jest-util": "30.0.5", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", "slash": "^3.0.0", "strip-bom": "^4.0.0" }, @@ -4163,9 +4164,9 @@ } }, "node_modules/jest-snapshot": { - "version": "30.1.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.1.2.tgz", - "integrity": "sha512-4q4+6+1c8B6Cy5pGgFvjDy/Pa6VYRiGu0yQafKkJ9u6wQx4G5PqI2QR6nxTl43yy7IWsINwz6oT4o6tD12a8Dg==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", + "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", "dev": true, "license": "MIT", "dependencies": { @@ -4174,20 +4175,20 @@ "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.27.1", "@babel/types": "^7.27.3", - "@jest/expect-utils": "30.1.2", + "@jest/expect-utils": "30.2.0", "@jest/get-type": "30.1.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", + "@jest/snapshot-utils": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0", "chalk": "^4.1.2", - "expect": "30.1.2", + "expect": "30.2.0", "graceful-fs": "^4.2.11", - "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", + "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", "semver": "^7.7.2", "synckit": "^0.11.8" }, @@ -4209,13 +4210,13 @@ } }, "node_modules/jest-util": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", - "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", + "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==", "dev": true, "license": "MIT", "dependencies": { - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "@types/node": "*", "chalk": "^4.1.2", "ci-info": "^4.2.0", @@ -4240,18 +4241,18 @@ } }, "node_modules/jest-validate": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.1.0.tgz", - "integrity": "sha512-7P3ZlCFW/vhfQ8pE7zW6Oi4EzvuB4sgR72Q1INfW9m0FGo0GADYlPwIkf4CyPq7wq85g+kPMtPOHNAdWHeBOaA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", + "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", "dev": true, "license": "MIT", "dependencies": { "@jest/get-type": "30.1.0", - "@jest/types": "30.0.5", + "@jest/types": "30.2.0", "camelcase": "^6.3.0", "chalk": "^4.1.2", "leven": "^3.1.0", - "pretty-format": "30.0.5" + "pretty-format": "30.2.0" }, "engines": { "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" @@ -4271,19 +4272,19 @@ } }, "node_modules/jest-watcher": { - "version": "30.1.3", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.1.3.tgz", - "integrity": "sha512-6jQUZCP1BTL2gvG9E4YF06Ytq4yMb4If6YoQGRR6PpjtqOXSP3sKe2kqwB6SQ+H9DezOfZaSLnmka1NtGm3fCQ==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", + "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", "dev": true, "license": "MIT", "dependencies": { - "@jest/test-result": "30.1.3", - "@jest/types": "30.0.5", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", "@types/node": "*", "ansi-escapes": "^4.3.2", "chalk": "^4.1.2", "emittery": "^0.13.1", - "jest-util": "30.0.5", + "jest-util": "30.2.0", "string-length": "^4.0.2" }, "engines": { @@ -4291,15 +4292,15 @@ } }, "node_modules/jest-worker": { - "version": "30.1.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.1.0.tgz", - "integrity": "sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", "dev": true, "license": "MIT", "dependencies": { "@types/node": "*", "@ungap/structured-clone": "^1.3.0", - "jest-util": "30.0.5", + "jest-util": "30.2.0", "merge-stream": "^2.0.0", "supports-color": "^8.1.1" }, @@ -4351,9 +4352,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "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==", + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", "dev": true, "license": "MIT", "dependencies": { @@ -4492,7 +4493,6 @@ "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,7 +4501,6 @@ "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" } @@ -4539,30 +4538,13 @@ } }, "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==", - "license": "MIT", + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", "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", @@ -4749,9 +4731,9 @@ "license": "MIT" }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.21", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", + "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", "dev": true, "license": "MIT" }, @@ -4843,7 +4825,6 @@ "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" }, @@ -5116,9 +5097,9 @@ } }, "node_modules/pretty-format": { - "version": "30.0.5", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", - "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "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==", "dev": true, "license": "MIT", "dependencies": { @@ -5150,7 +5131,6 @@ "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", - "peer": true, "dependencies": { "@prisma/config": "6.16.2", "@prisma/engines": "6.16.2" @@ -5242,7 +5222,6 @@ "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" }, @@ -5262,18 +5241,17 @@ } }, "node_modules/raw-body": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz", - "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==", - "license": "MIT", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.7.0", - "unpipe": "~1.0.0" + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" }, "engines": { - "node": ">= 0.10" + "node": ">= 0.8" } }, "node_modules/rc9": { @@ -5422,8 +5400,7 @@ "node_modules/safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "node_modules/semver": { "version": "6.3.1", @@ -5472,6 +5449,25 @@ } } }, + "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", @@ -5523,7 +5519,6 @@ "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", @@ -5542,7 +5537,6 @@ "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" @@ -5558,7 +5552,6 @@ "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", @@ -5576,7 +5569,6 @@ "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", @@ -5807,9 +5799,9 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "dev": true, "license": "MIT", "dependencies": { @@ -6048,7 +6040,6 @@ "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", @@ -6058,6 +6049,25 @@ "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", @@ -6082,7 +6092,6 @@ "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" } @@ -6347,9 +6356,9 @@ } }, "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "dev": true, "license": "MIT", "engines": { diff --git a/package.json b/package.json index 989093f..a09988c 100644 --- a/package.json +++ b/package.json @@ -14,13 +14,12 @@ "command-line-usage": "^7.0.3", "compression": "^1.8.1", "cors": "^2.8.5", - "express": "^5.2.1", + "express": "^5.0.0", "mqtt": "^5.14.1", - "protobufjs": "^7.5.4", - "ws": "^8.18.3" + "protobufjs": "^7.5.4" }, "devDependencies": { - "jest": "^30.1.3", + "jest": "^30.2.0", "prisma": "^6.16.2" } } diff --git a/prisma/migrations/20260106151912_add_edges/migration.sql b/prisma/migrations/20260106151912_add_edges/migration.sql deleted file mode 100644 index 113243f..0000000 --- a/prisma/migrations/20260106151912_add_edges/migration.sql +++ /dev/null @@ -1,23 +0,0 @@ --- 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 2a908dc..ef96a9c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -347,28 +347,4 @@ 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 25ee627..13d17b2 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/src/admin.js b/src/admin.js index 5889908..e9fbbef 100644 --- a/src/admin.js +++ b/src/admin.js @@ -1,7 +1,6 @@ // 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 48dbcc2..a51e465 100644 --- a/src/index.js +++ b/src/index.js @@ -1,4 +1,3 @@ -require('./utils/logger'); const path = require('path'); const express = require('express'); const compression = require('compression'); @@ -155,15 +154,6 @@ 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", @@ -229,10 +219,6 @@ 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 - } }, }); @@ -707,194 +693,6 @@ 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 67cb9dd..4852907 100644 --- a/src/mqtt.js +++ b/src/mqtt.js @@ -1,4 +1,3 @@ -require('./utils/logger'); const crypto = require("crypto"); const path = require("path"); const mqtt = require("mqtt"); @@ -984,12 +983,7 @@ client.on("message", async (topic, message) => { }, }); } catch (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); - } + console.error(e); } // Keep track of the names a node has been using. @@ -1012,12 +1006,7 @@ client.on("message", async (topic, message) => { } }); } catch (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); - } + console.error(e); } } @@ -1094,70 +1083,6 @@ 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; @@ -1400,160 +1325,6 @@ 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) { @@ -1658,7 +1429,6 @@ 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 c2e45a3..46b81e8 160000 --- a/src/protobufs +++ b/src/protobufs @@ -1 +1 @@ -Subproject commit c2e45a3fc9cda6aedb72ad3b5b88fcccfa78073e +Subproject commit 46b81e822af1b8e408f437092337f129dee693e6 diff --git a/src/public/assets/css/styles.css b/src/public/assets/css/styles.css deleted file mode 100644 index 8811cad..0000000 --- a/src/public/assets/css/styles.css +++ /dev/null @@ -1,115 +0,0 @@ -/* 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 deleted file mode 100644 index 86b4542..0000000 --- a/src/public/assets/js/app.js +++ /dev/null @@ -1,956 +0,0 @@ -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 deleted file mode 100644 index 5866a4c..0000000 --- a/src/public/assets/js/config.js +++ /dev/null @@ -1,199 +0,0 @@ -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 deleted file mode 100644 index e25c067..0000000 --- a/src/public/assets/js/map.js +++ /dev/null @@ -1,1692 +0,0 @@ -// 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)}` +
- `