diff --git a/README.md b/README.md index 854146a..2ee653b 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,11 @@ git clone https://github.com/liamcottle/meshtastic-map cd meshtastic-map ``` +Install Meshtastic protobufs definitions +``` +git clone https://github.com/meshtastic/protobufs src/protobufs +``` + Install NodeJS dependencies ``` diff --git a/package-lock.json b/package-lock.json index 2cbfa9b..1fb2318 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ }, "devDependencies": { "jest": "^30.1.3", - "prisma": "^7.4.1" + "prisma": "^6.16.2" } }, "node_modules/@ampproject/remapping": { @@ -69,6 +69,7 @@ "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -603,73 +604,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@chevrotain/cst-dts-gen": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-10.5.0.tgz", - "integrity": "sha512-lhmC/FyqQ2o7pGK4Om+hzuDrm9rhFYIJ/AXoQBeongmn870Xeb0L6oGEiuR8nohFNL5sMaQEJWCxr1oIVIVXrw==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/gast": "10.5.0", - "@chevrotain/types": "10.5.0", - "lodash": "4.17.21" - } - }, - "node_modules/@chevrotain/gast": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-10.5.0.tgz", - "integrity": "sha512-pXdMJ9XeDAbgOWKuD1Fldz4ieCs6+nLNmyVhe2gZVqoO7v8HXuHYs5OV2EzUtbuai37TlOAQHrTDvxMnvMJz3A==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/types": "10.5.0", - "lodash": "4.17.21" - } - }, - "node_modules/@chevrotain/types": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-10.5.0.tgz", - "integrity": "sha512-f1MAia0x/pAVPWH/T73BJVyO2XU5tI4/iE7cnxb7tqdNTNhQI3Uq3XkqcoteTmD4t1aM0LbHCJOhgIDn07kl2A==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@chevrotain/utils": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-10.5.0.tgz", - "integrity": "sha512-hBzuU5+JjB2cqNZyszkDHZgOSrUUT8V3dhgRl8Q9Gp6dAj/H5+KILGjbhDpc3Iy9qmqlm/akuOI2ut9VUtzJxQ==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@electric-sql/pglite": { - "version": "0.3.15", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite/-/pglite-0.3.15.tgz", - "integrity": "sha512-Cj++n1Mekf9ETfdc16TlDi+cDDQF0W7EcbyRHYOAeZdsAe8M/FJg18itDTSwyHfar2WIezawM9o0EKaRGVKygQ==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@electric-sql/pglite-socket": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-socket/-/pglite-socket-0.0.20.tgz", - "integrity": "sha512-J5nLGsicnD9wJHnno9r+DGxfcZWh+YJMCe0q/aCgtG6XOm9Z7fKeite8IZSNXgZeGltSigM9U/vAWZQWdgcSFg==", - "devOptional": true, - "license": "Apache-2.0", - "bin": { - "pglite-server": "dist/scripts/server.js" - }, - "peerDependencies": { - "@electric-sql/pglite": "0.3.15" - } - }, - "node_modules/@electric-sql/pglite-tools": { - "version": "0.2.20", - "resolved": "https://registry.npmjs.org/@electric-sql/pglite-tools/-/pglite-tools-0.2.20.tgz", - "integrity": "sha512-BK50ZnYa3IG7ztXhtgYf0Q7zijV32Iw1cYS8C+ThdQlwx12V5VZ9KRJ42y82Hyb4PkTxZQklVQA9JHyUlex33A==", - "devOptional": true, - "license": "Apache-2.0", - "peerDependencies": { - "@electric-sql/pglite": "0.3.15" - } - }, "node_modules/@emnapi/core": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", @@ -704,19 +638,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@hono/node-server": { - "version": "1.19.9", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.9.tgz", - "integrity": "sha512-vHL6w3ecZsky+8P5MD+eFfaGTyCeOHUIFYMGpQGbrBTSmNNoxv0if69rEZ5giu36weC5saFuznL411gRX7bJDw==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=18.14.1" - }, - "peerDependencies": { - "hono": "^4" - } - }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -1143,20 +1064,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@mrleebo/prisma-ast": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/@mrleebo/prisma-ast/-/prisma-ast-0.13.1.tgz", - "integrity": "sha512-XyroGQXcHrZdvmrGJvsA9KNeOOgGMg1Vg9OlheUsBOSKznLMDl+YChxbkboRHvtFYJEMRYmlV3uoo/njCw05iw==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "chevrotain": "^10.5.0", - "lilconfig": "^2.1.0" - }, - "engines": { - "node": ">=16" - } - }, "node_modules/@napi-rs/wasm-runtime": { "version": "0.2.12", "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", @@ -1217,138 +1124,66 @@ } }, "node_modules/@prisma/config": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@prisma/config/-/config-7.4.1.tgz", - "integrity": "sha512-vteSXm8N46bo3FW9MhPGVHAj+KRgrR6TWtlSk6GqToCKjTnOexXdPZyiDyEsfVW38YhqEmVl6w/6iHN8uYVJcw==", + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.16.2.tgz", + "integrity": "sha512-mKXSUrcqXj0LXWPmJsK2s3p9PN+aoAbyMx7m5E1v1FufofR1ZpPoIArjjzOIm+bJRLLvYftoNYLx1tbHgF9/yg==", "devOptional": true, "license": "Apache-2.0", "dependencies": { "c12": "3.1.0", "deepmerge-ts": "7.1.5", - "effect": "3.18.4", + "effect": "3.16.12", "empathic": "2.0.0" } }, "node_modules/@prisma/debug": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.4.1.tgz", - "integrity": "sha512-qEtzO8oLouRv18JDQUC3G3Gnv+fGVscHZm/x1DBB/WT+kOvPDQLM2woX6IGgWnSMYYlrxjuALshT7G/blvY0bQ==", + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.16.2.tgz", + "integrity": "sha512-bo4/gA/HVV6u8YK2uY6glhNsJ7r+k/i5iQ9ny/3q5bt9ijCj7WMPUwfTKPvtEgLP+/r26Z686ly11hhcLiQ8zA==", "devOptional": true, "license": "Apache-2.0" }, - "node_modules/@prisma/dev": { - "version": "0.20.0", - "resolved": "https://registry.npmjs.org/@prisma/dev/-/dev-0.20.0.tgz", - "integrity": "sha512-ovlBYwWor0OzG+yH4J3Ot+AneD818BttLA+Ii7wjbcLHUrnC4tbUPVGyNd3c/+71KETPKZfjhkTSpdS15dmXNQ==", - "devOptional": true, - "license": "ISC", - "dependencies": { - "@electric-sql/pglite": "0.3.15", - "@electric-sql/pglite-socket": "0.0.20", - "@electric-sql/pglite-tools": "0.2.20", - "@hono/node-server": "1.19.9", - "@mrleebo/prisma-ast": "0.13.1", - "@prisma/get-platform": "7.2.0", - "@prisma/query-plan-executor": "7.2.0", - "foreground-child": "3.3.1", - "get-port-please": "3.2.0", - "hono": "4.11.4", - "http-status-codes": "2.3.0", - "pathe": "2.0.3", - "proper-lockfile": "4.1.2", - "remeda": "2.33.4", - "std-env": "3.10.0", - "valibot": "1.2.0", - "zeptomatch": "2.1.0" - } - }, "node_modules/@prisma/engines": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-7.4.1.tgz", - "integrity": "sha512-BZEBdHvNJx5PzIG37EI/Zi5UUI5hGWjkYsQmKa7OIK6evAvebOTwutjS/VRI6cA6grmA52eLZR+oekGRMqkKxQ==", + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.16.2.tgz", + "integrity": "sha512-7yf3AjfPUgsg/l7JSu1iEhsmZZ/YE00yURPjTikqm2z4btM0bCl2coFtTGfeSOWbQMmq45Jab+53yGUIAT1sjA==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.4.1", - "@prisma/engines-version": "7.5.0-4.55ae170b1ced7fc6ed07a15f110549408c501bb3", - "@prisma/fetch-engine": "7.4.1", - "@prisma/get-platform": "7.4.1" + "@prisma/debug": "6.16.2", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/fetch-engine": "6.16.2", + "@prisma/get-platform": "6.16.2" } }, "node_modules/@prisma/engines-version": { - "version": "7.5.0-4.55ae170b1ced7fc6ed07a15f110549408c501bb3", - "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.5.0-4.55ae170b1ced7fc6ed07a15f110549408c501bb3.tgz", - "integrity": "sha512-fUxVd1TjOW8K4XsZ8dAm88sDW5Ry7AxWDfsYEWwScS6Fjo3caKC6hgNumUfsmsy0Il9LjDn5X0PpVXNt3iwayw==", + "version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43.tgz", + "integrity": "sha512-ThvlDaKIVrnrv97ujNFDYiQbeMQpLa0O86HFA2mNoip4mtFqM7U5GSz2ie1i2xByZtvPztJlNRgPsXGeM/kqAA==", "devOptional": true, "license": "Apache-2.0" }, - "node_modules/@prisma/engines/node_modules/@prisma/get-platform": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.4.1.tgz", - "integrity": "sha512-kN4tmkQzlgm/KtE+jTNSYjsDxxe/5i6GApPI32BN9T0tlgsgSBtDJbjGBICttkAIjsh73dXf8raPKxO/2n2UUg==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "7.4.1" - } - }, "node_modules/@prisma/fetch-engine": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-7.4.1.tgz", - "integrity": "sha512-Z9kbuxX2bvEsyeS3LZEiEnxG0lVtZbpYgaAnPj69N+A9f2De8Lta0EoFtld9zhfERVPIQWhSWUc8himky3qYdA==", + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.16.2.tgz", + "integrity": "sha512-wPnZ8DMRqpgzye758ZvfAMiNJRuYpz+rhgEBZi60ZqDIgOU2694oJxiuu3GKFeYeR/hXxso4/2oBC243t/whxQ==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.4.1", - "@prisma/engines-version": "7.5.0-4.55ae170b1ced7fc6ed07a15f110549408c501bb3", - "@prisma/get-platform": "7.4.1" - } - }, - "node_modules/@prisma/fetch-engine/node_modules/@prisma/get-platform": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.4.1.tgz", - "integrity": "sha512-kN4tmkQzlgm/KtE+jTNSYjsDxxe/5i6GApPI32BN9T0tlgsgSBtDJbjGBICttkAIjsh73dXf8raPKxO/2n2UUg==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@prisma/debug": "7.4.1" + "@prisma/debug": "6.16.2", + "@prisma/engines-version": "6.16.0-7.1c57fdcd7e44b29b9313256c76699e91c3ac3c43", + "@prisma/get-platform": "6.16.2" } }, "node_modules/@prisma/get-platform": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-7.2.0.tgz", - "integrity": "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==", + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.16.2.tgz", + "integrity": "sha512-U/P36Uke5wS7r1+omtAgJpEB94tlT4SdlgaeTc6HVTTT93pXj7zZ+B/cZnmnvjcNPfWddgoDx8RLjmQwqGDYyA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "@prisma/debug": "7.2.0" - } - }, - "node_modules/@prisma/get-platform/node_modules/@prisma/debug": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-7.2.0.tgz", - "integrity": "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/query-plan-executor": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@prisma/query-plan-executor/-/query-plan-executor-7.2.0.tgz", - "integrity": "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==", - "devOptional": true, - "license": "Apache-2.0" - }, - "node_modules/@prisma/studio-core": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/@prisma/studio-core/-/studio-core-0.13.1.tgz", - "integrity": "sha512-agdqaPEePRHcQ7CexEfkX1RvSH9uWDb6pXrZnhCRykhDFAV0/0P3d07WtfiY8hZWb7oRU4v+NkT4cGFHkQJIPg==", - "devOptional": true, - "license": "Apache-2.0", - "peerDependencies": { - "@types/react": "^18.0.0 || ^19.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" + "@prisma/debug": "6.16.2" } }, "node_modules/@protobufjs/aspromise": { @@ -1433,9 +1268,9 @@ } }, "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", "devOptional": true, "license": "MIT" }, @@ -1530,17 +1365,6 @@ "undici-types": "~5.26.4" } }, - "node_modules/@types/react": { - "version": "19.2.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz", - "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", - "devOptional": true, - "license": "MIT", - "peer": true, - "dependencies": { - "csstype": "^3.2.2" - } - }, "node_modules/@types/readable-stream": { "version": "4.0.21", "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.21.tgz", @@ -1947,16 +1771,6 @@ "node": ">=12.17" } }, - "node_modules/aws-ssl-profiles": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", - "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 6.0.0" - } - }, "node_modules/babel-jest": { "version": "30.1.2", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.1.2.tgz", @@ -2196,6 +2010,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001737", "electron-to-chromium": "^1.5.211", @@ -2395,21 +2210,6 @@ "node": ">=10" } }, - "node_modules/chevrotain": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-10.5.0.tgz", - "integrity": "sha512-Pkv5rBY3+CsHOYfV5g/Vs5JY9WTHHDEKOlohI2XeygaZhUeqhAlldZ8Hz9cRmxu709bvS08YzxHdTPHhffc13A==", - "devOptional": true, - "license": "Apache-2.0", - "dependencies": { - "@chevrotain/cst-dts-gen": "10.5.0", - "@chevrotain/gast": "10.5.0", - "@chevrotain/types": "10.5.0", - "@chevrotain/utils": "10.5.0", - "lodash": "4.17.21", - "regexp-to-ast": "0.5.0" - } - }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -2681,9 +2481,9 @@ } }, "node_modules/confbox": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", - "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", "devOptional": true, "license": "MIT" }, @@ -2756,7 +2556,7 @@ "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "path-key": "^3.1.0", @@ -2767,14 +2567,6 @@ "node": ">= 8" } }, - "node_modules/csstype": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", - "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", - "devOptional": true, - "license": "MIT", - "peer": true - }, "node_modules/debug": { "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", @@ -2825,16 +2617,6 @@ "devOptional": true, "license": "MIT" }, - "node_modules/denque": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", - "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", - "devOptional": true, - "license": "Apache-2.0", - "engines": { - "node": ">=0.10" - } - }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -2900,9 +2682,9 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/effect": { - "version": "3.18.4", - "resolved": "https://registry.npmjs.org/effect/-/effect-3.18.4.tgz", - "integrity": "sha512-b1LXQJLe9D11wfnOKAk3PKxuqYshQ0Heez+y5pnkd3jLj1yx9QhM72zZ9uUrOQyNvrs2GZZd/3maL0ZV18YuDA==", + "version": "3.16.12", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.16.12.tgz", + "integrity": "sha512-N39iBk0K71F9nb442TLbTkjl24FLUzuvx2i1I2RsEAQsdAdUTuUoW0vlfUXgkMTUOnYqKnWcFfqw4hK4Pw27hg==", "devOptional": true, "license": "MIT", "dependencies": { @@ -3204,9 +2986,9 @@ } }, "node_modules/exsolve": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", "devOptional": true, "license": "MIT" }, @@ -3365,7 +3147,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "cross-spawn": "^7.0.6", @@ -3425,16 +3207,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/generate-function": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", - "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "is-property": "^1.0.2" - } - }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3489,13 +3261,6 @@ "node": ">=8.0.0" } }, - "node_modules/get-port-please": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/get-port-please/-/get-port-please-3.2.0.tgz", - "integrity": "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==", - "devOptional": true, - "license": "MIT" - }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -3577,23 +3342,9 @@ "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "devOptional": true, + "dev": true, "license": "ISC" }, - "node_modules/grammex": { - "version": "3.1.12", - "resolved": "https://registry.npmjs.org/grammex/-/grammex-3.1.12.tgz", - "integrity": "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/graphmatch": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/graphmatch/-/graphmatch-1.1.1.tgz", - "integrity": "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==", - "devOptional": true, - "license": "MIT" - }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", @@ -3632,16 +3383,6 @@ "integrity": "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg==", "license": "MIT" }, - "node_modules/hono": { - "version": "4.11.4", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.4.tgz", - "integrity": "sha512-U7tt8JsyrxSRKspfhtLET79pU8K+tInj5QZXs1jSugO1Vq5dFj3kmZsRldo29mTBfcjDRVRXrEZ6LS63Cog9ZA==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=16.9.0" - } - }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -3669,13 +3410,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-status-codes": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/http-status-codes/-/http-status-codes-2.3.0.tgz", - "integrity": "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==", - "devOptional": true, - "license": "MIT" - }, "node_modules/human-signals": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", @@ -3838,13 +3572,6 @@ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" }, - "node_modules/is-property": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", - "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", - "devOptional": true, - "license": "MIT" - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -3862,7 +3589,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "devOptional": true, + "dev": true, "license": "ISC" }, "node_modules/istanbul-lib-coverage": { @@ -4597,9 +4324,9 @@ } }, "node_modules/jiti": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", - "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.5.1.tgz", + "integrity": "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w==", "devOptional": true, "license": "MIT", "bin": { @@ -4686,16 +4413,6 @@ "node": ">=6" } }, - "node_modules/lilconfig": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -4716,13 +4433,6 @@ "node": ">=8" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "devOptional": true, - "license": "MIT" - }, "node_modules/lodash.camelcase": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", @@ -4739,22 +4449,6 @@ "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "license": "ISC" }, - "node_modules/lru.min": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz", - "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==", - "devOptional": true, - "license": "MIT", - "engines": { - "bun": ">=1.0.0", - "deno": ">=1.30.0", - "node": ">=8.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wellwelwel" - } - }, "node_modules/make-dir": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", @@ -5008,40 +4702,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, - "node_modules/mysql2": { - "version": "3.15.3", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.15.3.tgz", - "integrity": "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "aws-ssl-profiles": "^1.1.1", - "denque": "^2.1.0", - "generate-function": "^2.3.1", - "iconv-lite": "^0.7.0", - "long": "^5.2.1", - "lru.min": "^1.0.0", - "named-placeholders": "^1.1.3", - "seq-queue": "^0.0.5", - "sqlstring": "^2.3.2" - }, - "engines": { - "node": ">= 8.0" - } - }, - "node_modules/named-placeholders": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz", - "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "lru.min": "^1.1.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/napi-postinstall": { "version": "0.3.3", "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", @@ -5152,30 +4812,25 @@ "license": "MIT" }, "node_modules/nypm": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", - "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", + "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", "devOptional": true, "license": "MIT", "dependencies": { - "citty": "^0.2.0", + "citty": "^0.1.6", + "consola": "^3.4.2", "pathe": "^2.0.3", - "tinyexec": "^1.0.2" + "pkg-types": "^2.3.0", + "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" }, "engines": { - "node": ">=18" + "node": "^14.16.0 || >=16.10.0" } }, - "node_modules/nypm/node_modules/citty": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", - "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", - "devOptional": true, - "license": "MIT" - }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5360,7 +5015,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -5460,20 +5115,6 @@ "pathe": "^2.0.3" } }, - "node_modules/postgres": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/postgres/-/postgres-3.4.7.tgz", - "integrity": "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==", - "devOptional": true, - "license": "Unlicense", - "engines": { - "node": ">=12" - }, - "funding": { - "type": "individual", - "url": "https://github.com/sponsors/porsager" - } - }, "node_modules/pretty-format": { "version": "30.0.5", "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", @@ -5503,34 +5144,27 @@ } }, "node_modules/prisma": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/prisma/-/prisma-7.4.1.tgz", - "integrity": "sha512-gDKOXwnPiMdB+uYMhMeN8jj4K7Cu3Q2wB/wUsITOoOk446HtVb8T9BZxFJ1Zop6alc89k6PMNdR2FZCpbXp/jw==", + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.16.2.tgz", + "integrity": "sha512-aRvldGE5UUJTtVmFiH3WfNFNiqFlAtePUxcI0UEGlnXCX7DqhiMT5TRYwncHFeA/Reca5W6ToXXyCMTeFPdSXA==", "devOptional": true, "hasInstallScript": true, "license": "Apache-2.0", + "peer": true, "dependencies": { - "@prisma/config": "7.4.1", - "@prisma/dev": "0.20.0", - "@prisma/engines": "7.4.1", - "@prisma/studio-core": "0.13.1", - "mysql2": "3.15.3", - "postgres": "3.4.7" + "@prisma/config": "6.16.2", + "@prisma/engines": "6.16.2" }, "bin": { "prisma": "build/index.js" }, "engines": { - "node": "^20.19 || ^22.12 || >=24.0" + "node": ">=18.18" }, "peerDependencies": { - "better-sqlite3": ">=9.0.0", - "typescript": ">=5.4.0" + "typescript": ">=5.1.0" }, "peerDependenciesMeta": { - "better-sqlite3": { - "optional": true - }, "typescript": { "optional": true } @@ -5551,25 +5185,6 @@ "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", "license": "MIT" }, - "node_modules/proper-lockfile": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", - "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "retry": "^0.12.0", - "signal-exit": "^3.0.2" - } - }, - "node_modules/proper-lockfile/node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "devOptional": true, - "license": "ISC" - }, "node_modules/protobufjs": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", @@ -5672,31 +5287,6 @@ "destr": "^2.0.3" } }, - "node_modules/react": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.4.tgz", - "integrity": "sha512-9nfp2hYpCwOjAN+8TZFGhtWEwgvWHXqESH8qT89AT/lWklpLON22Lc8pEtnpsZz7VmawabSU0gCjnj8aC0euHQ==", - "devOptional": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==", - "devOptional": true, - "license": "MIT", - "peer": true, - "dependencies": { - "scheduler": "^0.27.0" - }, - "peerDependencies": { - "react": "^19.2.4" - } - }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -5734,23 +5324,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/regexp-to-ast": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/regexp-to-ast/-/regexp-to-ast-0.5.0.tgz", - "integrity": "sha512-tlbJqcMHnPKI9zSrystikWKwHkBqu2a/Sgw01h3zFjvYrMxEDYHzzoMZnUrbIfpTFEsoRnnviOXNCzFiSc54Qw==", - "devOptional": true, - "license": "MIT" - }, - "node_modules/remeda": { - "version": "2.33.4", - "resolved": "https://registry.npmjs.org/remeda/-/remeda-2.33.4.tgz", - "integrity": "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==", - "devOptional": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/remeda" - } - }, "node_modules/require-directory": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", @@ -5784,16 +5357,6 @@ "node": ">=8" } }, - "node_modules/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, "node_modules/rfdc": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", @@ -5862,14 +5425,6 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, - "node_modules/scheduler": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", - "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", - "devOptional": true, - "license": "MIT", - "peer": true - }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -5922,12 +5477,6 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, - "node_modules/seq-queue": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", - "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==", - "devOptional": true - }, "node_modules/serve-static": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", @@ -5951,7 +5500,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "devOptional": true, + "dev": true, "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" @@ -5964,7 +5513,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "devOptional": true, + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -6046,7 +5595,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", - "devOptional": true, + "dev": true, "license": "ISC", "engines": { "node": ">=14" @@ -6126,16 +5675,6 @@ "dev": true, "license": "BSD-3-Clause" }, - "node_modules/sqlstring": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", - "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -6157,13 +5696,6 @@ "node": ">= 0.8" } }, - "node_modules/std-env": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", - "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", - "devOptional": true, - "license": "MIT" - }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -6449,14 +5981,11 @@ } }, "node_modules/tinyexec": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", - "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=18" - } + "license": "MIT" }, "node_modules/tmpl": { "version": "1.0.5", @@ -6645,21 +6174,6 @@ "node": ">=10.12.0" } }, - "node_modules/valibot": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/valibot/-/valibot-1.2.0.tgz", - "integrity": "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==", - "devOptional": true, - "license": "MIT", - "peerDependencies": { - "typescript": ">=5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -6682,7 +6196,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "devOptional": true, + "dev": true, "license": "ISC", "dependencies": { "isexe": "^2.0.0" @@ -6988,17 +6502,6 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } - }, - "node_modules/zeptomatch": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/zeptomatch/-/zeptomatch-2.1.0.tgz", - "integrity": "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==", - "devOptional": true, - "license": "MIT", - "dependencies": { - "grammex": "^3.1.11", - "graphmatch": "^1.1.0" - } } } } diff --git a/package.json b/package.json index b4285ae..989093f 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,6 @@ }, "devDependencies": { "jest": "^30.1.3", - "prisma": "^7.4.1" + "prisma": "^6.16.2" } } diff --git a/screenshot.png b/screenshot.png index 13d17b2..25ee627 100644 Binary files a/screenshot.png and b/screenshot.png differ diff --git a/src/public/assets/css/styles.css b/src/public/assets/css/styles.css new file mode 100644 index 0000000..8811cad --- /dev/null +++ b/src/public/assets/css/styles.css @@ -0,0 +1,115 @@ +/* used to prevent ui flicker before vuejs loads */ +[v-cloak] { + display: none; +} + +.icon-longfast { + background-color: #009016; + border-radius: 25px; + border: 1px solid #2C2D3C; +} + +.icon-mediumfast { + background-color: #326be7; + border-radius: 25px; + border: 1px solid #2C2D3C; +} + +.icon-shortslow { + background-color: #0077e6; + border-radius: 25px; + border: 1px solid #2C2D3C; +} + +.icon-mqtt-connected { + background-color: #2563eb; /* Change to use same color as disconnected // #16a34a; */ + border-radius: 25px; + border: 1px solid #2C2D3C; +} + +.icon-mqtt-disconnected { + background-color: #2563eb; + border-radius: 25px; + border: 1px solid #2C2D3C; +} + +.icon-offline { + background-color: #e2286c; + border-radius: 25px; + border: 1px solid #2C2D3C; +} + +.icon-position-history { + background-color: #a855f7; + border-radius: 25px; + border: 1px solid #2C2D3C; +} + +.icon-traceroute-start { + background-color: #16a34a; /* green */ + border-radius: 25px; + border: 1px solid #2C2D3C; +} + +.icon-traceroute-end { + background-color: #dc2626; /* red */ + border-radius: 25px; + border: 1px solid #2C2D3C; +} + +.waypoint-label { + font-size: 26px; + background-color: transparent; +} + +.link { + color: #2563eb; +} + +.link:hover { + text-decoration: underline; +} + +.tooltip { + position: relative; + display: inline-block; +} + +.tooltip .tooltip-text { + visibility: hidden; + width: 80px; + background-color: black; + color: #fff; + text-align: center; + padding: 4px 0; + border-radius: 6px; + position: absolute; + z-index: 10000; + top: 100%; + left: 50%; + margin-top: 8px; + margin-left: -40px; /* Use half of the width (120/2 = 60), to center the tooltip */ +} + +.tooltip .tooltip-text::after { + content: " "; + position: absolute; + bottom: 100%; /* At the top of the tooltip */ + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: transparent transparent black transparent; +} + +.tooltip:hover .tooltip-text { + visibility: visible; +} + +.z-search { + z-index: 1001; +} + +.z-sidebar { + z-index: 1002; +} \ No newline at end of file diff --git a/src/public/assets/js/app.js b/src/public/assets/js/app.js new file mode 100644 index 0000000..86b4542 --- /dev/null +++ b/src/public/assets/js/app.js @@ -0,0 +1,956 @@ +Vue.createApp({ + data() { + return { + + isShowingAnnouncement: this.shouldShowAnnouncement(), + + configNodesMaxAgeInSeconds: window.getConfigNodesMaxAgeInSeconds(), + configNodesOfflineAgeInSeconds: window.getConfigNodesOfflineAgeInSeconds(), + configWaypointsMaxAgeInSeconds: window.getConfigWaypointsMaxAgeInSeconds(), + configConnectionsMaxDistanceInMeters: window.getConfigConnectionsMaxDistanceInMeters(), + configZoomLevelGoToNode: window.getConfigZoomLevelGoToNode(), + configAutoUpdatePositionInUrl: window.getConfigAutoUpdatePositionInUrl(), + configEnableMapAnimations: window.getConfigEnableMapAnimations(), + configTemperatureFormat: window.getConfigTemperatureFormat(), + configConnectionsTimePeriodInSeconds: window.getConfigConnectionsTimePeriodInSeconds(), + configConnectionsColoredLines: window.getConfigConnectionsColoredLines(), + configConnectionsBidirectionalOnly: window.getConfigConnectionsBidirectionalOnly(), + configConnectionsMinSnrDb: window.getConfigConnectionsMinSnrDb(), + configConnectionsBidirectionalMinSnr: window.getConfigConnectionsBidirectionalMinSnr(), + + isShowingHardwareModels: false, + hardwareModelStats: null, + + isShowingInfoModal: this.shouldShowInfoModal(), + isShowingMobileSearch: false, + isShowingSettings: false, + + nodes: [], + searchText: "", + + selectedNode: null, + selectedNodeDeviceMetrics: [], + selectedNodeEnvironmentMetrics: [], + selectedNodePowerMetrics: [], + selectedNodeMqttMetrics: [], + selectedNodeTraceroutes: [], + + deviceMetricsTimeRange: "7d", + environmentMetricsTimeRange: "7d", + powerMetricsTimeRange: "7d", + + isPositionHistoryModalExpanded: true, + positionHistoryDateTimeFrom: null, + positionHistoryDateTimeTo: null, + selectedNodePositionHistory: [], + selectedNodeToShowPositionHistory: null, + selectedNodePositionHistoryMarkers: [], + selectedNodePositionHistoryPolyLines: [], + + selectedTraceRoute: null, + tracerouteEdges: [], + + selectedNodeToShowConnections: null, + + moment: window.moment, + + }; + }, + mounted: function() { + + // load data + this.loadHardwareModelStats(); + + // handle map click callback from outside of vue + window._onMapClick = () => { + this.searchText = ""; + this.isShowingMobileSearch = false; + }; + + // handle node callback from outside of vue + window._onNodeClick = (node) => { + this.selectedNode = node; + this.loadNodeDeviceMetrics(node.node_id); + this.loadNodeEnvironmentMetrics(node.node_id); + this.loadNodePowerMetrics(node.node_id); + this.loadNodeMqttMetrics(node.node_id); + this.loadNodeTraceroutes(node.node_id); + //this.loadNodePositionHistory(node.node_id); + }; + + // handle node callback from outside of vue + window._onShowNodeConnectionsClick = (node) => { + this.selectedNodeToShowConnections = node; + }; + + // handle nodes updated callback from outside of vue + window._onNodesUpdated = (nodes) => { + this.nodes = nodes; + }; + + }, + methods: { + getAnnouncementId: function() { + // change this when making a new announcement + return "1"; + }, + shouldShowAnnouncement: function() { + const lastSeenAnnouncementId = window.localStorage.getItem("last-seen-announcement-id"); + return lastSeenAnnouncementId?.toString() !== this.getAnnouncementId(); + }, + dismissAnnouncement: function() { + window.localStorage.setItem("last-seen-announcement-id", this.getAnnouncementId()); + this.isShowingAnnouncement = false; + }, + shouldShowInfoModal: function() { + return !window.getConfigHasSeenInfoModal() + && !window.isMobile(); + }, + loadHardwareModelStats: function() { + window.axios.get('/api/v1/stats/hardware-models').then((response) => { + this.hardwareModelStats = response.data.hardware_model_stats; + }).catch((error) => { + // do nothing + }); + }, + loadNodeDeviceMetrics: function(nodeId) { + + // calculate unix timestamps in milliseconds for supported time ranges + const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000); + const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000); + const sevenDaysAgoInMilliseconds = new Date().getTime() - (604800 * 1000); + const thirtyDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000 * 10); + + // determine how long back to load device metrics from + var timeFrom = threeDaysAgoInMilliseconds; + switch(this.deviceMetricsTimeRange){ + case "1d": { + timeFrom = oneDayAgoInMilliseconds; + break; + } + case "3d": { + timeFrom = threeDaysAgoInMilliseconds; + break; + } + case "7d": { + timeFrom = sevenDaysAgoInMilliseconds; + break; + } + case "30d": { + timeFrom = thirtyDaysAgoInMilliseconds; + break; + } + } + + window.axios.get(`/api/v1/nodes/${nodeId}/device-metrics`, { + params: { + time_from: timeFrom, + }, + }).then((response) => { + // reverse response, as it's newest to oldest, but we want oldest to newest + this.selectedNodeDeviceMetrics = response.data.device_metrics.reverse(); + this.renderDeviceMetricCharts(); + }).catch(() => { + this.selectedNodeDeviceMetrics = []; + this.renderDeviceMetricCharts(); + }); + }, + loadNodeEnvironmentMetrics: function(nodeId) { + + // calculate unix timestamps in milliseconds for supported time ranges + const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000); + const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000); + const sevenDaysAgoInMilliseconds = new Date().getTime() - (604800 * 1000); + const thirtyDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000 * 10); + + // determine how long back to load environment metrics from + var timeFrom = threeDaysAgoInMilliseconds; + switch(this.environmentMetricsTimeRange){ + case "1d": { + timeFrom = oneDayAgoInMilliseconds; + break; + } + case "3d": { + timeFrom = threeDaysAgoInMilliseconds; + break; + } + case "7d": { + timeFrom = sevenDaysAgoInMilliseconds; + break; + } + case "30d": { + timeFrom = thirtyDaysAgoInMilliseconds; + break; + } + } + + window.axios.get(`/api/v1/nodes/${nodeId}/environment-metrics`, { + params: { + time_from: timeFrom, + }, + }).then((response) => { + // reverse response, as it's newest to oldest, but we want oldest to newest + this.selectedNodeEnvironmentMetrics = response.data.environment_metrics.reverse(); + this.renderEnvironmentMetricCharts(); + }).catch(() => { + this.selectedNodeEnvironmentMetrics = []; + this.renderEnvironmentMetricCharts(); + }); + }, + loadNodePowerMetrics: function(nodeId) { + + // calculate unix timestamps in milliseconds for supported time ranges + const oneDayAgoInMilliseconds = new Date().getTime() - (86400 * 1000); + const threeDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000); + const sevenDaysAgoInMilliseconds = new Date().getTime() - (604800 * 1000); + const thirtyDaysAgoInMilliseconds = new Date().getTime() - (259200 * 1000 * 10); + + // determine how long back to load power metrics from + var timeFrom = threeDaysAgoInMilliseconds; + switch(this.powerMetricsTimeRange){ + case "1d": { + timeFrom = oneDayAgoInMilliseconds; + break; + } + case "3d": { + timeFrom = threeDaysAgoInMilliseconds; + break; + } + case "7d": { + timeFrom = sevenDaysAgoInMilliseconds; + break; + } + case "30d": { + timeFrom = thirtyDaysAgoInMilliseconds; + break; + } + } + + window.axios.get(`/api/v1/nodes/${nodeId}/power-metrics`, { + params: { + time_from: timeFrom, + }, + }).then((response) => { + // reverse response, as it's newest to oldest, but we want oldest to newest + this.selectedNodePowerMetrics = response.data.power_metrics.reverse(); + this.renderPowerMetricCharts(); + }).catch(() => { + this.selectedNodePowerMetrics = []; + this.renderPowerMetricCharts(); + }); + }, + loadNodeMqttMetrics: function(nodeId) { + this.selectedNodeMqttMetrics = []; + window.axios.get(`/api/v1/nodes/${nodeId}/mqtt-metrics`).then((response) => { + this.selectedNodeMqttMetrics = response.data.mqtt_metrics; + }).catch(() => { + // do nothing + }); + }, + loadNodeTraceroutes: function(nodeId) { + this.selectedNodeTraceroutes = []; + window.axios.get(`/api/v1/nodes/${nodeId}/traceroutes`, { + params: { + count: 5, + }, + }).then((response) => { + this.selectedNodeTraceroutes = response.data.traceroutes; + }).catch(() => { + // do nothing + }); + }, + loadNodePositionHistory: function(nodeId) { + this.selectedNodePositionHistory = []; + window.axios.get(`/api/v1/nodes/${nodeId}/position-history`, { + params: { + // parse from datetime-local format, and send as unix timestamp in milliseconds + time_from: moment(this.positionHistoryDateTimeFrom, "YYYY-MM-DDTHH:mm").format("x"), + time_to: moment(this.positionHistoryDateTimeTo, "YYYY-MM-DDTHH:mm").format("x"), + }, + }).then((response) => { + this.selectedNodePositionHistory = response.data.position_history; + if(this.selectedNodeToShowPositionHistory != null){ + clearAllPositionHistory(); + onPositionHistoryUpdated(response.data.position_history); + } + + }).catch(() => { + // do nothing + }); + }, + renderDeviceMetricCharts: function() { + try { + this.updateDeviceMetricsChart(); + } catch(e) { + console.log(e); + } + }, + updateDeviceMetricsChart: function() { + + // destroy existing chart + const chartElementId = "deviceMetricsChart"; + const existingChart = window.Chart.getChart(chartElementId); + if(existingChart != null){ + existingChart.destroy(); + } + + // get chart element + const chartElement = window.document.getElementById(chartElementId); + if(!chartElement){ + return; + } + + // create chart data + const labels = []; + const batteryMetrics = []; + const channelUtilizationMetrics = []; + const airUtilTxMetrics = []; + for(const deviceMetric of this.selectedNodeDeviceMetrics){ + labels.push(moment(deviceMetric.created_at)); + batteryMetrics.push(deviceMetric.battery_level); + channelUtilizationMetrics.push(deviceMetric.channel_utilization); + airUtilTxMetrics.push(deviceMetric.air_util_tx); + } + + // create chart + new window.Chart(chartElement, { + type: 'line', + data: { + labels: labels, + datasets: [ + { + label: 'Battery Level', + borderColor: '#3b82f6', + backgroundColor: '#3b82f6', + pointStyle: false, // no points + fill: false, + data: batteryMetrics, + }, + { + label: 'Channel Util', + borderColor: '#22c55e', + backgroundColor: '#22c55e', + showLine: false, // no lines between points + fill: false, + data: channelUtilizationMetrics, + }, + { + label: 'Air Util TX', + borderColor: '#f97316', + backgroundColor: '#f97316', + showLine: false, // no lines between points + fill: false, + data: airUtilTxMetrics, + + }, + ], + }, + options: { + responsive: true, + borderWidth: 2, + elements: { + point: { + radius: 2, + }, + }, + scales: { + x: { + position: 'top', + type: 'time', + time: { + unit: 'day', + displayFormats: { + day: 'MMM DD', // Jan 01 + }, + }, + }, + y: { + min: 0, + max: 101, // 101 is "Plugged In", need to include for tooltip to work + ticks: { + callback: (label) => `${label}%`, + }, + }, + }, + plugins: { + legend: { + display: false, + }, + tooltip: { + mode: "index", + intersect: false, + callbacks: { + label: (item) => { + return `${item.dataset.label}: ${item.formattedValue}%`; + }, + }, + }, + }, + } + }); + + }, + renderEnvironmentMetricCharts: function() { + try { + this.updateEnvironmentMetricsChart(); + } catch(e) { + console.log(e); + } + }, + updateEnvironmentMetricsChart: function() { + + // destroy existing chart + const chartElementId = "environmentMetricsChart"; + const existingChart = window.Chart.getChart(chartElementId); + if(existingChart != null){ + existingChart.destroy(); + } + + // get chart element + const chartElement = window.document.getElementById(chartElementId); + if(!chartElement){ + return; + } + + // create chart data + const labels = []; + const temperatureMetrics = []; + const relativeHumidityMetrics = []; + const barometricPressureMetrics = []; + const iaqMetrics = []; + for(const deviceMetric of this.selectedNodeEnvironmentMetrics){ + labels.push(moment(deviceMetric.created_at)); + temperatureMetrics.push(deviceMetric.temperature); + relativeHumidityMetrics.push(deviceMetric.relative_humidity); + barometricPressureMetrics.push(deviceMetric.barometric_pressure); + iaqMetrics.push(deviceMetric.iaq); + } + + // create chart + new window.Chart(chartElement, { + type: 'line', + data: { + labels: labels, + datasets: [ + { + label: 'Temperature', + suffix: '°C', + borderColor: '#3b82f6', + backgroundColor: '#3b82f6', + pointStyle: false, // no points + fill: false, + data: temperatureMetrics, + yAxisID: 'y', + }, + { + label: 'Humidity', + suffix: '%', + borderColor: '#22c55e', + backgroundColor: '#22c55e', + pointStyle: false, // no points + fill: false, + data: relativeHumidityMetrics, + yAxisID: 'y', + }, + { + label: 'Pressure', + suffix: 'hPa', + borderColor: '#f97316', + backgroundColor: '#f97316', + pointStyle: false, // no points + fill: false, + data: barometricPressureMetrics, + yAxisID: 'y1', + + }, + { + label: 'IAQ', + suffix: 'IAQ', + borderColor: '#f472b6', + backgroundColor: '#f472b6', + pointStyle: false, // no points + fill: false, + data: iaqMetrics, + yAxisID: 'yIAQ', + + }, + ], + }, + options: { + responsive: true, + borderWidth: 2, + spanGaps: 1000 * 60 * 60 * 24, // only show lines between metrics with a 24 hour or less gap + elements: { + point: { + radius: 2, + }, + }, + scales: { + x: { + position: 'top', + type: 'time', + time: { + unit: 'day', + displayFormats: { + day: 'MMM DD', // Jan 01 + }, + }, + }, + y: { + min: -20, + max: 100, + }, + y1: { + min: 800, + max: 1100, + ticks: { + stepSize: 10, + callback: (label) => `${label} hPa`, + }, + position: 'right', + grid: { + drawOnChartArea: false, // only want the grid lines for one axis to show up + }, + }, + yIAQ: { + type: 'linear', + display: false, + }, + }, + plugins: { + legend: { + display: false, + }, + tooltip: { + mode: "index", + intersect: false, + callbacks: { + label: (item) => { + return `${item.dataset.label}: ${item.formattedValue}${item.dataset.suffix}`; + }, + }, + }, + }, + } + }); + + }, + renderPowerMetricCharts: function() { + try { + this.updatePowerMetricsChart(); + } catch(e) { + console.log(e); + } + }, + updatePowerMetricsChart: function() { + + // destroy existing chart + const chartElementId = "powerMetricsChart"; + const existingChart = window.Chart.getChart(chartElementId); + if(existingChart != null){ + existingChart.destroy(); + } + + // get chart element + const chartElement = window.document.getElementById(chartElementId); + if(!chartElement){ + return; + } + + // create chart data + const labels = []; + const channel1VoltageReadings = []; + const channel2VoltageReadings = []; + const channel3VoltageReadings = []; + const channel1CurrentReadings = []; + const channel2CurrentReadings = []; + const channel3CurrentReadings = []; + for(const powerMetric of this.selectedNodePowerMetrics){ + labels.push(moment(powerMetric.created_at)); + channel1VoltageReadings.push(powerMetric.ch1_voltage); + channel2VoltageReadings.push(powerMetric.ch2_voltage); + channel3VoltageReadings.push(powerMetric.ch3_voltage); + channel1CurrentReadings.push(powerMetric.ch1_current); + channel2CurrentReadings.push(powerMetric.ch2_current); + channel3CurrentReadings.push(powerMetric.ch3_current); + } + + // create chart + new window.Chart(chartElement, { + type: 'line', + data: { + labels: labels, + datasets: [ + { + label: 'Ch1 Voltage', + suffix: "V", + borderColor: '#3b82f6', + backgroundColor: '#3b82f6', + pointStyle: false, // no points + fill: false, + data: channel1VoltageReadings, + yAxisID: 'y', + }, + { + label: 'Ch2 Voltage', + suffix: "V", + borderColor: '#22c55e', + backgroundColor: '#22c55e', + pointStyle: false, // no points + fill: false, + data: channel2VoltageReadings, + yAxisID: 'y', + }, + { + label: 'Ch3 Voltage', + suffix: "V", + borderColor: '#f97316', + backgroundColor: '#f97316', + pointStyle: false, // no points + fill: false, + data: channel3VoltageReadings, + yAxisID: 'y', + }, + { + label: 'Ch1 Current', + suffix: "mA", + borderColor: '#93c5fd', + backgroundColor: '#93c5fd', + pointStyle: false, // no points + fill: false, + data: channel1CurrentReadings, + yAxisID: 'y1', + }, + { + label: 'Ch2 Current', + suffix: "mA", + borderColor: '#86efac', + backgroundColor: '#86efac', + pointStyle: false, // no points + fill: false, + data: channel2CurrentReadings, + yAxisID: 'y1', + }, + { + label: 'Ch3 Current', + suffix: "mA", + borderColor: '#fdba74', + backgroundColor: '#fdba74', + pointStyle: false, // no points + fill: false, + data: channel3CurrentReadings, + yAxisID: 'y1', + }, + ], + }, + options: { + responsive: true, + borderWidth: 2, + spanGaps: 1000 * 60 * 60 * 3, // only show lines between metrics with a 3 hour or less gap + elements: { + point: { + radius: 2, + }, + }, + scales: { + x: { + position: 'top', + type: 'time', + time: { + unit: 'day', + displayFormats: { + day: 'MMM DD', // Jan 01 + }, + }, + }, + y: { + min: 0, + suggestedMax: 6, + ticks: { + callback: (label) => `${label}V`, + }, + }, + y1: { + suggestedMin: -50, + suggestedMax: 50, + ticks: { + stepSize: 50, + callback: (label) => `${label}mA`, + }, + position: 'right', + grid: { + drawOnChartArea: false, // only want the grid lines for one axis to show up + }, + }, + }, + plugins: { + legend: { + display: false, + }, + tooltip: { + mode: "index", + intersect: false, + callbacks: { + label: (item) => { + return `${item.dataset.label}: ${item.formattedValue}${item.dataset.suffix}`; + }, + }, + }, + }, + } + }); + + }, + showTraceRoute: function(traceroute) { + this.selectedTraceRoute = traceroute; + }, + findNodeById: function(id) { + return window.findNodeById(id); + }, + findNodeMarkerById: function(id) { + return window.findNodeMarkerById(id); + }, + onSearchResultNodeClick: function(node) { + + // clear search + this.searchText = ""; + + // hide search + this.isShowingMobileSearch = false; + + // go to node + if(window.goToNode(node.node_id)){ + return; + } + + // fallback to showing node details since we can't go to the node + window.showNodeDetails(node.node_id); + + }, + dismissInfoModal: function() { + this.isShowingInfoModal = false; + window.setConfigHasSeenInfoModal(true); + }, + getRegionFrequencyRange: function(regionName) { + return window.getRegionFrequencyRange(regionName); + }, + showNodePositionHistory: function(nodeId) { + + // find node + const node = findNodeById(nodeId); + if(!node){ + return; + } + + // update ui + this.selectedNode = null; + this.selectedNodeToShowPositionHistory = node; + this.isPositionHistoryModalExpanded = true; + + // close node info tooltip as position history shows under it + window.closeAllTooltips(); + + // reset default time range when opening position history ui + // YYYY-MM-DDTHH:mm is the format expected by the datetime-local input type + this.positionHistoryDateTimeFrom = moment().subtract(1, "hours").format('YYYY-MM-DDTHH:mm'); + this.positionHistoryDateTimeTo = moment().format('YYYY-MM-DDTHH:mm'); + + // load position history + this.loadNodePositionHistory(nodeId); + + }, + onPositionHistoryQuickRangeClick: function(range) { + + // update position history time range + switch(range){ + case "1h": { + this.positionHistoryDateTimeFrom = moment().subtract(1, "hours").format('YYYY-MM-DDTHH:mm'); + this.positionHistoryDateTimeTo = moment().format('YYYY-MM-DDTHH:mm'); + break; + } + case "24h": { + this.positionHistoryDateTimeFrom = moment().subtract(24, "hours").format('YYYY-MM-DDTHH:mm'); + this.positionHistoryDateTimeTo = moment().format('YYYY-MM-DDTHH:mm'); + break; + } + case "7d": { + this.positionHistoryDateTimeFrom = moment().subtract(7, "days").format('YYYY-MM-DDTHH:mm'); + this.positionHistoryDateTimeTo = moment().format('YYYY-MM-DDTHH:mm'); + break; + } + } + + // reload position history + const node = this.selectedNodeToShowPositionHistory; + if(node){ + this.loadNodePositionHistory(node.node_id); + } + + }, + getShareLinkForNode: function(nodeId) { + return window.location.origin + `/?node_id=${nodeId}`; + }, + copyShareLinkForNode: function(nodeId) { + + // make sure copy to clipboard is supported + if(!navigator.clipboard || !navigator.clipboard.writeText){ + alert("Clipboard not supported. Site must be served via https on iOS."); + return; + } + + // copy share link to clipboard + const url = this.getShareLinkForNode(nodeId); + navigator.clipboard.writeText(url); + + // tell user we copied it + alert("Link copied to clipboard!"); + + }, + dismissShowingNodeConnections: function() { + window._onHideNodeConnectionsClick(); + this.selectedNodeToShowConnections = null; + }, + dismissShowingNodePositionHistory: function() { + this.selectedNodePositionHistory = []; + this.selectedNodeToShowPositionHistory = null; + this.selectedNodePositionHistoryMarkers = []; + this.selectedNodePositionHistoryPolyLines = []; + cleanUpPositionHistory(); + }, + formatUptimeSeconds: function(secondsToFormat) { + secondsToFormat = Number(secondsToFormat); + var days = Math.floor(secondsToFormat / (3600 * 24)); + var hours = Math.floor((secondsToFormat % (3600 * 24)) / 3600); + var minutes = Math.floor((secondsToFormat % 3600) / 60); + var seconds = Math.floor(secondsToFormat % 60); + var daysPlural = days === 1 ? 'day' : 'days'; + return `${days} ${daysPlural} ${hours}h ${minutes}m ${seconds}s`; + }, + formatTemperature: function(celsius) { + switch(this.configTemperatureFormat){ + case "celsius": { + return `${Number(celsius).toFixed(0)}°C`; + } + case "fahrenheit": { + const fahrenheit = this.celsiusToFahrenheit(celsius); + return `${fahrenheit.toFixed(0)}°F`; + } + } + }, + convertTemperature: function(celsius) { + switch(this.configTemperatureFormat){ + case "celsius": { + return celsius; + } + case "fahrenheit": { + return this.celsiusToFahrenheit(celsius); + } + } + }, + getTemperatureUnit: function() { + switch(this.configTemperatureFormat){ + case "celsius": return "°C"; + case "fahrenheit": return "°F"; + } + }, + celsiusToFahrenheit: function(celsius) { + return (celsius * 9/5) + 32; + }, + getNodeColour(nodeId) { + // convert node id to a hex colour + return "#" + (nodeId & 0x00FFFFFF).toString(16).padStart(6, '0'); + }, + getNodeTextColour(nodeId) { + + // extract rgb components + const r = (nodeId & 0xFF0000) >> 16; + const g = (nodeId & 0x00FF00) >> 8; + const b = nodeId & 0x0000FF; + + // calculate brightness + const brightness = ((r * 0.299) + (g * 0.587) + (b * 0.114)) / 255; + + // determine text color based on brightness + return brightness > 0.5 ? "#000000" : "#FFFFFF"; + + }, + }, + computed: { + searchedNodes() { + + // search nodes + const nodes = this.nodes.filter((node) => { + const matchesId = node.node_id?.toLowerCase()?.includes(this.searchText.toLowerCase()); + const matchesHexId = node.node_id_hex?.toLowerCase()?.includes(this.searchText.toLowerCase()); + const matchesLongName = node.long_name?.toLowerCase()?.includes(this.searchText.toLowerCase()); + const matchesShortName = node.short_name?.toLowerCase()?.includes(this.searchText.toLowerCase()); + return matchesId || matchesHexId || matchesLongName || matchesShortName; + }); + + // order alphabetically by long name + nodes.sort((nodeA, nodeB) => { + const nodeALongName = nodeA.long_name || ""; + const nodeBLongName = nodeB.long_name || ""; + return nodeALongName.localeCompare(nodeBLongName); + }); + + // only return the first 500 results to avoid ui lag... + return nodes.slice(0, 500); + + }, + selectedNodeLatestPowerMetric() { + const [ latestPowerMetric ] = this.selectedNodePowerMetrics.slice(-1); + return latestPowerMetric; + }, + }, + watch: { + configNodesMaxAgeInSeconds() { + window.setConfigNodesMaxAgeInSeconds(this.configNodesMaxAgeInSeconds); + }, + configNodesOfflineAgeInSeconds() { + window.setConfigNodesOfflineAgeInSeconds(this.configNodesOfflineAgeInSeconds); + }, + configWaypointsMaxAgeInSeconds() { + window.setConfigWaypointsMaxAgeInSeconds(this.configWaypointsMaxAgeInSeconds); + }, + configConnectionsMaxDistanceInMeters() { + window.setConfigConnectionsMaxDistanceInMeters(this.configConnectionsMaxDistanceInMeters); + }, + configZoomLevelGoToNode() { + window.setConfigZoomLevelGoToNode(this.configZoomLevelGoToNode); + }, + configAutoUpdatePositionInUrl() { + window.setConfigAutoUpdatePositionInUrl(this.configAutoUpdatePositionInUrl); + }, + configEnableMapAnimations() { + window.setConfigEnableMapAnimations(this.configEnableMapAnimations); + }, + configTemperatureFormat() { + window.setConfigTemperatureFormat(this.configTemperatureFormat); + }, + configConnectionsTimePeriodInSeconds() { + window.setConfigConnectionsTimePeriodInSeconds(this.configConnectionsTimePeriodInSeconds); + }, + configConnectionsColoredLines() { + window.setConfigConnectionsColoredLines(this.configConnectionsColoredLines); + }, + configConnectionsBidirectionalOnly() { + window.setConfigConnectionsBidirectionalOnly(this.configConnectionsBidirectionalOnly); + }, + configConnectionsMinSnrDb() { + window.setConfigConnectionsMinSnrDb(this.configConnectionsMinSnrDb); + }, + configConnectionsBidirectionalMinSnr() { + window.setConfigConnectionsBidirectionalMinSnr(this.configConnectionsBidirectionalMinSnr); + }, + deviceMetricsTimeRange() { + this.loadNodeDeviceMetrics(this.selectedNode.node_id); + }, + environmentMetricsTimeRange() { + this.loadNodeEnvironmentMetrics(this.selectedNode.node_id); + }, + powerMetricsTimeRange() { + this.loadNodePowerMetrics(this.selectedNode.node_id); + }, + }, +}).mount('#app'); \ No newline at end of file diff --git a/src/public/assets/js/config.js b/src/public/assets/js/config.js new file mode 100644 index 0000000..5866a4c --- /dev/null +++ b/src/public/assets/js/config.js @@ -0,0 +1,199 @@ +function getConfigHasSeenInfoModal() { + return localStorage.getItem("config_has_seen_info_modal") === "true"; +} + +function setConfigHasSeenInfoModal(value) { + return localStorage.setItem("config_has_seen_info_modal", value); +} + +function getConfigAutoUpdatePositionInUrl() { + // use user preference, or enable by default + const value = localStorage.getItem("config_auto_update_position_in_url"); + return value === "true" || value == null; +} + +function setConfigAutoUpdatePositionInUrl(value) { + return localStorage.setItem("config_auto_update_position_in_url", value); +} + +function getConfigEnableMapAnimations() { + + const value = localStorage.getItem("config_enable_map_animations"); + + // enable animations by default + if(value === null){ + return true; + } + + return value === "true"; + +} + +function setConfigEnableMapAnimations(value) { + return localStorage.setItem("config_enable_map_animations", value); +} + +function getConfigTemperatureFormat() { + return localStorage.getItem("config_temperature_format") || "celsius"; +} + +function setConfigTemperatureFormat(format) { + return localStorage.setItem("config_temperature_format", format); +} + +function getConfigMapSelectedTileLayer() { + return localStorage.getItem("config_map_selected_tile_layer") || "Thunderforest Neighbourhood"; +} + +function setConfigMapSelectedTileLayer(layer) { + return localStorage.setItem("config_map_selected_tile_layer", layer); +} + +function getConfigMapEnabledOverlayLayers() { + + try { + const value = localStorage.getItem("config_map_enabled_overlay_layers"); + if(value){ + return JSON.parse(value); + } + } catch(e) {} + + // overlays enabled by default + return ["Legend", "Position History", "Traceroutes"]; + +} + +function setConfigMapEnabledOverlayLayers(layers) { + return localStorage.setItem("config_map_enabled_overlay_layers", JSON.stringify(layers)); +} + +function getConfigNodesMaxAgeInSeconds() { + const value = localStorage.getItem("config_nodes_max_age_in_seconds"); + return value != null ? parseInt(value) : null; +} + +function setConfigNodesMaxAgeInSeconds(value) { + if(value != null){ + return localStorage.setItem("config_nodes_max_age_in_seconds", value); + } else { + return localStorage.removeItem("config_nodes_max_age_in_seconds"); + } +} + +function getConfigNodesOfflineAgeInSeconds() { + const value = localStorage.getItem("config_nodes_offline_age_in_seconds"); + return value != null ? parseInt(value) : 10800; +} + +function setConfigNodesOfflineAgeInSeconds(value) { + if(value != null){ + return localStorage.setItem("config_nodes_offline_age_in_seconds", value); + } else { + return localStorage.removeItem("config_nodes_offline_age_in_seconds"); + } +} + +function getConfigWaypointsMaxAgeInSeconds() { + const value = localStorage.getItem("config_waypoints_max_age_in_seconds"); + return value != null ? parseInt(value) : null; +} + +function setConfigWaypointsMaxAgeInSeconds(value) { + if(value != null){ + return localStorage.setItem("config_waypoints_max_age_in_seconds", value); + } else { + return localStorage.removeItem("config_waypoints_max_age_in_seconds"); + } +} + +function getConfigConnectionsMaxDistanceInMeters() { + const value = localStorage.getItem("config_connections_max_distance_in_meters"); + // default to 70km (70,000 meters) + return value != null ? parseInt(value) : 70000; +} + +function setConfigConnectionsMaxDistanceInMeters(value) { + return localStorage.setItem("config_connections_max_distance_in_meters", value); +} + +function getConfigZoomLevelGoToNode() { + const value = localStorage.getItem("config_zoom_level_go_to_node"); + const parsedValue = value != null ? parseInt(value) : null; + return parsedValue || 15; +} + +function setConfigZoomLevelGoToNode(value) { + return localStorage.setItem("config_zoom_level_go_to_node", value); +} + +function getConfigConnectionsTimePeriodInSeconds() { + const value = localStorage.getItem("config_connections_time_period_in_seconds"); + // default to 7 days if unset + return value != null ? parseInt(value) : 604800; +} + +function setConfigConnectionsTimePeriodInSeconds(value) { + return localStorage.setItem("config_connections_time_period_in_seconds", value); +} + +function getConfigConnectionsColoredLines() { + const value = localStorage.getItem("config_connections_colored_lines"); + // disable colored lines by default + if(value === null){ + return false; + } + return value === "true"; +} + +function setConfigConnectionsColoredLines(value) { + return localStorage.setItem("config_connections_colored_lines", value); +} + +function getConfigConnectionsBidirectionalOnly() { + const value = localStorage.getItem("config_connections_bidirectional_only"); + // disable bidirectional filter by default + if(value === null){ + return false; + } + return value === "true"; +} + +function setConfigConnectionsBidirectionalOnly(value) { + return localStorage.setItem("config_connections_bidirectional_only", value); +} + +function getConfigConnectionsMinSnrDb() { + const value = localStorage.getItem("config_connections_min_snr_db"); + // default to null (unset) + if(value === null || value === ""){ + return null; + } + const parsed = parseFloat(value); + return isNaN(parsed) ? null : parsed; +} + +function setConfigConnectionsMinSnrDb(value) { + if(value === null || value === "" || value === undefined){ + return localStorage.removeItem("config_connections_min_snr_db"); + } + // Convert to string for localStorage (handles both number and string inputs) + const stringValue = typeof value === "number" ? value.toString() : String(value); + return localStorage.setItem("config_connections_min_snr_db", stringValue); +} + +function getConfigConnectionsBidirectionalMinSnr() { + const value = localStorage.getItem("config_connections_bidirectional_min_snr"); + // disable bidirectional minimum SNR by default + if(value === null){ + return false; + } + return value === "true"; +} + +function setConfigConnectionsBidirectionalMinSnr(value) { + return localStorage.setItem("config_connections_bidirectional_min_snr", value); +} + +function isMobile() { + return /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent); +} \ No newline at end of file diff --git a/src/public/assets/js/map.js b/src/public/assets/js/map.js new file mode 100644 index 0000000..e25c067 --- /dev/null +++ b/src/public/assets/js/map.js @@ -0,0 +1,1692 @@ +// global state +var nodes = []; +var nodeMarkers = {}; +var selectedNodeOutlineCircle = null; +var waypoints = []; + +// set map bounds to be a little more than full size to prevent panning off screen +var bounds = [ + [-100, 70], // top left + [100, 500], // bottom right +]; + +// create map positioned over NRW +if(!isMobile()){ + var map = L.map('map', { + maxBounds: bounds, + }).setView([ + 51.1, + 366.82, + ], 9); +} else { + var map = L.map('map', { + maxBounds: bounds, + }).setView([ + 51.1, + 366.82, + ], 8); +} + +// remove leaflet link +map.attributionControl.setPrefix(''); + +var openThunderforestLandscapeMapTileLayer = L.tileLayer('https://tiles.nixware.dev/landscape/{z}/{x}/{y}.png', { + maxZoom: 22, + attribution: 'Tiles © Gravitystorm Limited | Data from Meshtastic', +}); + +var openThunderforestAtlasMapTileLayer = L.tileLayer('https://tiles.nixware.dev/atlas/{z}/{x}/{y}.png', { + maxZoom: 22, + attribution: 'Tiles © Gravitystorm Limited | Data from Meshtastic', +}); + +var openThunderforestNeighbourhoodMapTileLayer = L.tileLayer('https://tiles.nixware.dev/neighbourhood/{z}/{x}/{y}.png', { + maxZoom: 22, + attribution: 'Tiles © Gravitystorm Limited | Data from Meshtastic', +}); + +var openStreetMapTileLayer = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', { + maxZoom: 22, // increase from 18 to 22 + attribution: 'Tiles © OpenStreetMap | Data from Meshtastic', +}); + +var openTopoMapTileLayer = L.tileLayer('https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png', { + maxZoom: 17, // open topo map doesn't have tiles closer than this + attribution: 'Tiles © OpenStreetMap | Data from Meshtastic', +}); + +var esriWorldImageryTileLayer = L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}', { + maxZoom: 21, // esri doesn't have tiles closer than this + attribution: 'Tiles © Esri | Data from Meshtastic' +}); + +var googleSatelliteTileLayer = L.tileLayer('https://{s}.google.com/vt/lyrs=s&x={x}&y={y}&z={z}', { + maxZoom: 21, + subdomains: ['mt0', 'mt1', 'mt2', 'mt3'], + attribution: 'Tiles © Google | Data from Meshtastic' +}); + +var googleHybridTileLayer = L.tileLayer('https://{s}.google.com/vt/lyrs=s,h&x={x}&y={y}&z={z}', { + maxZoom: 21, + subdomains: ['mt0', 'mt1', 'mt2', 'mt3'], + attribution: 'Tiles © Google | Data from Meshtastic' +}); + +var tileLayers = { + "Thunderforest Neighbourhood": openThunderforestNeighbourhoodMapTileLayer, + "Thunderforest Landscape": openThunderforestLandscapeMapTileLayer, + "Thunderforest Atlas": openThunderforestAtlasMapTileLayer, + "OpenStreetMap": openStreetMapTileLayer, + "OpenTopoMap": openTopoMapTileLayer, + "Esri Satellite": esriWorldImageryTileLayer, + "Google Satellite": googleSatelliteTileLayer, + "Google Hybrid": googleHybridTileLayer, +}; + +// use tile layer based on config +const selectedTileLayerName = getConfigMapSelectedTileLayer(); +const selectedTileLayer = tileLayers[selectedTileLayerName] || openThunderforestNeighbourhoodMapTileLayer; +selectedTileLayer.addTo(map); + +// create layer groups +var nodesLayerGroup = new L.LayerGroup(); +var backboneConnectionsLayerGroup = new L.LayerGroup(); +var nodeConnectionsLayerGroup = new L.LayerGroup(); +var nodesClusteredLayerGroup = L.markerClusterGroup({ + showCoverageOnHover: false, + disableClusteringAtZoom: 10, // zoom level where node clustering is disabled +}); +var nodesRouterLayerGroup = L.markerClusterGroup({ + showCoverageOnHover: false, + disableClusteringAtZoom: 10, // zoom level where node clustering is disabled +}); +var nodesBackboneLayerGroup = new L.LayerGroup(); +//var nodesMediumFastLayerGroup = new L.LayerGroup(); +var nodesShortSlowLayerGroup = new L.LayerGroup(); +var nodesLongFastLayerGroup = new L.LayerGroup(); +var waypointsLayerGroup = new L.LayerGroup(); +var nodePositionHistoryLayerGroup = new L.LayerGroup(); +var traceroutesLayerGroup = new L.LayerGroup(); +var connectionsLayerGroup = new L.LayerGroup(); + +// create icons +var iconMqttConnected = L.divIcon({ + className: 'icon-mqtt-connected', + iconSize: [16, 16], // increase from 12px to 16px to make hover easier +}); + +var iconLongFast = L.divIcon({ + className: 'icon-longfast', + iconSize: [16, 16], // increase from 12px to 16px to make hover easier +}); + +/*var iconMediumFast = L.divIcon({ + className: 'icon-mediumfast', + iconSize: [16, 16], // increase from 12px to 16px to make hover easier +});*/ + +var iconShortSlow = L.divIcon({ + className: 'icon-shortslow', + iconSize: [16, 16], +}); + +var iconMqttDisconnected = L.divIcon({ + className: 'icon-mqtt-disconnected', + iconSize: [16, 16], // increase from 12px to 16px to make hover easier +}); + +var iconOffline = L.divIcon({ + className: 'icon-offline', + iconSize: [16, 16], // increase from 12px to 16px to make hover easier +}); + +var iconPositionHistory = L.divIcon({ + className: 'icon-position-history', + iconSize: [16, 16], // increase from 12px to 16px to make hover easier +}); + +var iconTracerouteStart = L.divIcon({ + className: 'icon-traceroute-start', + iconSize: [16, 16], +}); + +var iconTracerouteEnd = L.divIcon({ + className: 'icon-traceroute-end', + iconSize: [16, 16], +}); + +// create legend +var legendLayerGroup = new L.LayerGroup(); +var legend = L.control({position: 'bottomleft'}); +legend.onAdd = function (map) { + var div = L.DomUtil.create('div', 'leaflet-control-layers'); + div.style.backgroundColor = 'white'; + div.style.padding = '12px'; + div.innerHTML = `
` +
+ `${escapeString(node.long_name)}` +
+ `