Merge branch 'master' into unread-counts
This commit is contained in:
+1
-1
@@ -2,6 +2,6 @@ dist
|
|||||||
node_modules
|
node_modules
|
||||||
stats.html
|
stats.html
|
||||||
.vercel
|
.vercel
|
||||||
.vite/deps
|
.vite
|
||||||
dev-dist
|
dev-dist
|
||||||
__screenshots__*
|
__screenshots__*
|
||||||
@@ -139,46 +139,28 @@ reasons:
|
|||||||
- **Web Standard APIs**: Uses browser-compatible APIs, making code more portable
|
- **Web Standard APIs**: Uses browser-compatible APIs, making code more portable
|
||||||
between server and client environments.
|
between server and client environments.
|
||||||
|
|
||||||
### Debugging
|
### Contributing
|
||||||
|
|
||||||
#### Debugging with React Scan
|
We welcome contributions! Here’s how the deployment flow works for pull
|
||||||
|
requests:
|
||||||
|
|
||||||
Meshtastic Web Client has included the library
|
- **Preview Deployments:**\
|
||||||
[React Scan](https://github.com/aidenybai/react-scan) to help you identify and
|
Every pull request automatically generates a preview deployment on Vercel.
|
||||||
resolve render performance issues during development.
|
This allows you and reviewers to easily preview changes before merging.
|
||||||
|
|
||||||
React's comparison-by-reference approach to props makes it easy to inadvertently
|
- **Staging Environment (`client-test`):**\
|
||||||
cause unnecessary re-renders, especially with:
|
Once your PR is merged, your changes will be available on our staging site:
|
||||||
|
[client-test.meshtastic.org](https://client-test.meshtastic.org/).\
|
||||||
|
This environment supports rapid feature iteration and testing without
|
||||||
|
impacting the production site.
|
||||||
|
|
||||||
- Inline function callbacks (`onClick={() => handleClick()}`)
|
- **Production Releases:**\
|
||||||
- Object literals (`style={{ color: "purple" }}`)
|
At regular intervals, stable and fully tested releases are promoted to our
|
||||||
- Array literals (`items={[1, 2, 3]}`)
|
production site: [client.meshtastic.org](https://client.meshtastic.org/).\
|
||||||
|
This is the primary interface used by the public to connect with their
|
||||||
|
Meshtastic nodes.
|
||||||
|
|
||||||
These are recreated on every render, causing child components to re-render even
|
Please review our
|
||||||
when nothing has actually changed.
|
[Contribution Guidelines](https://github.com/meshtastic/web/blob/master/CONTRIBUTING.md)
|
||||||
|
before submitting a pull request. We appreciate your help in making the project
|
||||||
Unlike React DevTools, React Scan specifically focuses on performance
|
better!
|
||||||
optimization by:
|
|
||||||
|
|
||||||
- Clearly distinguishing between necessary and unnecessary renders
|
|
||||||
- Providing render counts for components
|
|
||||||
- Highlighting slow-rendering components
|
|
||||||
- Offering a dedicated performance debugging experience
|
|
||||||
|
|
||||||
#### Usage
|
|
||||||
|
|
||||||
When experiencing slow renders, run:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
deno task dev:scan
|
|
||||||
```
|
|
||||||
|
|
||||||
This will allow you to discover the following about your components and pages:
|
|
||||||
|
|
||||||
- Components with excessive re-renders
|
|
||||||
- Performance bottlenecks in the render tree
|
|
||||||
- Expensive hook operations
|
|
||||||
- Props that change reference on every render
|
|
||||||
|
|
||||||
Use these insights to apply targeted optimizations like `React.memo()`,
|
|
||||||
`useCallback()`, or `useMemo()` where they'll have the most impact.
|
|
||||||
|
|||||||
@@ -56,7 +56,6 @@
|
|||||||
"npm:react-hook-form@^7.54.2": "7.54.2_react@19.0.0",
|
"npm:react-hook-form@^7.54.2": "7.54.2_react@19.0.0",
|
||||||
"npm:react-map-gl@8.0.1": "8.0.1_maplibre-gl@5.1.1_react@19.0.0_react-dom@19.0.0__react@19.0.0",
|
"npm:react-map-gl@8.0.1": "8.0.1_maplibre-gl@5.1.1_react@19.0.0_react-dom@19.0.0__react@19.0.0",
|
||||||
"npm:react-qrcode-logo@3": "3.0.0_react@19.0.0_react-dom@19.0.0__react@19.0.0",
|
"npm:react-qrcode-logo@3": "3.0.0_react@19.0.0_react-dom@19.0.0__react@19.0.0",
|
||||||
"npm:react-scan@~0.2.8": "0.2.8_react@19.0.0_react-dom@19.0.0__react@19.0.0_preact@10.26.4",
|
|
||||||
"npm:react@19": "19.0.0",
|
"npm:react@19": "19.0.0",
|
||||||
"npm:rfc4648@^1.5.4": "1.5.4",
|
"npm:rfc4648@^1.5.4": "1.5.4",
|
||||||
"npm:simple-git-hooks@^2.11.1": "2.11.1",
|
"npm:simple-git-hooks@^2.11.1": "2.11.1",
|
||||||
@@ -899,168 +898,78 @@
|
|||||||
"tough-cookie"
|
"tough-cookie"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@clack/core@0.3.5": {
|
|
||||||
"integrity": "sha512-5cfhQNH+1VQ2xLQlmzXMqUoiaH0lRBq9/CLW9lTyMbuKLC3+xEK01tHVvyut++mLOn5urSHmkm6I0Lg9MaJSTQ==",
|
|
||||||
"dependencies": [
|
|
||||||
"picocolors",
|
|
||||||
"sisteransi"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"@clack/prompts@0.8.2": {
|
|
||||||
"integrity": "sha512-6b9Ab2UiZwJYA9iMyboYyW9yJvAO9V753ZhS+DHKEjZRKAxPPOb7MXXu84lsPFG+vZt6FRFniZ8rXi+zCIw4yQ==",
|
|
||||||
"dependencies": [
|
|
||||||
"@clack/core",
|
|
||||||
"picocolors",
|
|
||||||
"sisteransi"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"@esbuild/aix-ppc64@0.24.2": {
|
|
||||||
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA=="
|
|
||||||
},
|
|
||||||
"@esbuild/aix-ppc64@0.25.0": {
|
"@esbuild/aix-ppc64@0.25.0": {
|
||||||
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ=="
|
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ=="
|
||||||
},
|
},
|
||||||
"@esbuild/android-arm64@0.24.2": {
|
|
||||||
"integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg=="
|
|
||||||
},
|
|
||||||
"@esbuild/android-arm64@0.25.0": {
|
"@esbuild/android-arm64@0.25.0": {
|
||||||
"integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g=="
|
"integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g=="
|
||||||
},
|
},
|
||||||
"@esbuild/android-arm@0.24.2": {
|
|
||||||
"integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q=="
|
|
||||||
},
|
|
||||||
"@esbuild/android-arm@0.25.0": {
|
"@esbuild/android-arm@0.25.0": {
|
||||||
"integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g=="
|
"integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g=="
|
||||||
},
|
},
|
||||||
"@esbuild/android-x64@0.24.2": {
|
|
||||||
"integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw=="
|
|
||||||
},
|
|
||||||
"@esbuild/android-x64@0.25.0": {
|
"@esbuild/android-x64@0.25.0": {
|
||||||
"integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg=="
|
"integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg=="
|
||||||
},
|
},
|
||||||
"@esbuild/darwin-arm64@0.24.2": {
|
|
||||||
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA=="
|
|
||||||
},
|
|
||||||
"@esbuild/darwin-arm64@0.25.0": {
|
"@esbuild/darwin-arm64@0.25.0": {
|
||||||
"integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw=="
|
"integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw=="
|
||||||
},
|
},
|
||||||
"@esbuild/darwin-x64@0.24.2": {
|
|
||||||
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA=="
|
|
||||||
},
|
|
||||||
"@esbuild/darwin-x64@0.25.0": {
|
"@esbuild/darwin-x64@0.25.0": {
|
||||||
"integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg=="
|
"integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg=="
|
||||||
},
|
},
|
||||||
"@esbuild/freebsd-arm64@0.24.2": {
|
|
||||||
"integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg=="
|
|
||||||
},
|
|
||||||
"@esbuild/freebsd-arm64@0.25.0": {
|
"@esbuild/freebsd-arm64@0.25.0": {
|
||||||
"integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w=="
|
"integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w=="
|
||||||
},
|
},
|
||||||
"@esbuild/freebsd-x64@0.24.2": {
|
|
||||||
"integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q=="
|
|
||||||
},
|
|
||||||
"@esbuild/freebsd-x64@0.25.0": {
|
"@esbuild/freebsd-x64@0.25.0": {
|
||||||
"integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A=="
|
"integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A=="
|
||||||
},
|
},
|
||||||
"@esbuild/linux-arm64@0.24.2": {
|
|
||||||
"integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg=="
|
|
||||||
},
|
|
||||||
"@esbuild/linux-arm64@0.25.0": {
|
"@esbuild/linux-arm64@0.25.0": {
|
||||||
"integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg=="
|
"integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg=="
|
||||||
},
|
},
|
||||||
"@esbuild/linux-arm@0.24.2": {
|
|
||||||
"integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA=="
|
|
||||||
},
|
|
||||||
"@esbuild/linux-arm@0.25.0": {
|
"@esbuild/linux-arm@0.25.0": {
|
||||||
"integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg=="
|
"integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg=="
|
||||||
},
|
},
|
||||||
"@esbuild/linux-ia32@0.24.2": {
|
|
||||||
"integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw=="
|
|
||||||
},
|
|
||||||
"@esbuild/linux-ia32@0.25.0": {
|
"@esbuild/linux-ia32@0.25.0": {
|
||||||
"integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg=="
|
"integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg=="
|
||||||
},
|
},
|
||||||
"@esbuild/linux-loong64@0.24.2": {
|
|
||||||
"integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ=="
|
|
||||||
},
|
|
||||||
"@esbuild/linux-loong64@0.25.0": {
|
"@esbuild/linux-loong64@0.25.0": {
|
||||||
"integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw=="
|
"integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw=="
|
||||||
},
|
},
|
||||||
"@esbuild/linux-mips64el@0.24.2": {
|
|
||||||
"integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw=="
|
|
||||||
},
|
|
||||||
"@esbuild/linux-mips64el@0.25.0": {
|
"@esbuild/linux-mips64el@0.25.0": {
|
||||||
"integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ=="
|
"integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ=="
|
||||||
},
|
},
|
||||||
"@esbuild/linux-ppc64@0.24.2": {
|
|
||||||
"integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw=="
|
|
||||||
},
|
|
||||||
"@esbuild/linux-ppc64@0.25.0": {
|
"@esbuild/linux-ppc64@0.25.0": {
|
||||||
"integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw=="
|
"integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw=="
|
||||||
},
|
},
|
||||||
"@esbuild/linux-riscv64@0.24.2": {
|
|
||||||
"integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q=="
|
|
||||||
},
|
|
||||||
"@esbuild/linux-riscv64@0.25.0": {
|
"@esbuild/linux-riscv64@0.25.0": {
|
||||||
"integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA=="
|
"integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA=="
|
||||||
},
|
},
|
||||||
"@esbuild/linux-s390x@0.24.2": {
|
|
||||||
"integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw=="
|
|
||||||
},
|
|
||||||
"@esbuild/linux-s390x@0.25.0": {
|
"@esbuild/linux-s390x@0.25.0": {
|
||||||
"integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA=="
|
"integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA=="
|
||||||
},
|
},
|
||||||
"@esbuild/linux-x64@0.24.2": {
|
|
||||||
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q=="
|
|
||||||
},
|
|
||||||
"@esbuild/linux-x64@0.25.0": {
|
"@esbuild/linux-x64@0.25.0": {
|
||||||
"integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw=="
|
"integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw=="
|
||||||
},
|
},
|
||||||
"@esbuild/netbsd-arm64@0.24.2": {
|
|
||||||
"integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw=="
|
|
||||||
},
|
|
||||||
"@esbuild/netbsd-arm64@0.25.0": {
|
"@esbuild/netbsd-arm64@0.25.0": {
|
||||||
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw=="
|
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw=="
|
||||||
},
|
},
|
||||||
"@esbuild/netbsd-x64@0.24.2": {
|
|
||||||
"integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw=="
|
|
||||||
},
|
|
||||||
"@esbuild/netbsd-x64@0.25.0": {
|
"@esbuild/netbsd-x64@0.25.0": {
|
||||||
"integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA=="
|
"integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA=="
|
||||||
},
|
},
|
||||||
"@esbuild/openbsd-arm64@0.24.2": {
|
|
||||||
"integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A=="
|
|
||||||
},
|
|
||||||
"@esbuild/openbsd-arm64@0.25.0": {
|
"@esbuild/openbsd-arm64@0.25.0": {
|
||||||
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw=="
|
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw=="
|
||||||
},
|
},
|
||||||
"@esbuild/openbsd-x64@0.24.2": {
|
|
||||||
"integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA=="
|
|
||||||
},
|
|
||||||
"@esbuild/openbsd-x64@0.25.0": {
|
"@esbuild/openbsd-x64@0.25.0": {
|
||||||
"integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg=="
|
"integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg=="
|
||||||
},
|
},
|
||||||
"@esbuild/sunos-x64@0.24.2": {
|
|
||||||
"integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig=="
|
|
||||||
},
|
|
||||||
"@esbuild/sunos-x64@0.25.0": {
|
"@esbuild/sunos-x64@0.25.0": {
|
||||||
"integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg=="
|
"integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg=="
|
||||||
},
|
},
|
||||||
"@esbuild/win32-arm64@0.24.2": {
|
|
||||||
"integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ=="
|
|
||||||
},
|
|
||||||
"@esbuild/win32-arm64@0.25.0": {
|
"@esbuild/win32-arm64@0.25.0": {
|
||||||
"integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw=="
|
"integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw=="
|
||||||
},
|
},
|
||||||
"@esbuild/win32-ia32@0.24.2": {
|
|
||||||
"integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA=="
|
|
||||||
},
|
|
||||||
"@esbuild/win32-ia32@0.25.0": {
|
"@esbuild/win32-ia32@0.25.0": {
|
||||||
"integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA=="
|
"integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA=="
|
||||||
},
|
},
|
||||||
"@esbuild/win32-x64@0.24.2": {
|
|
||||||
"integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg=="
|
|
||||||
},
|
|
||||||
"@esbuild/win32-x64@0.25.0": {
|
"@esbuild/win32-x64@0.25.0": {
|
||||||
"integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ=="
|
"integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ=="
|
||||||
},
|
},
|
||||||
@@ -1099,7 +1008,7 @@
|
|||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@inquirer/core",
|
"@inquirer/core",
|
||||||
"@inquirer/type",
|
"@inquirer/type",
|
||||||
"@types/node@22.13.8"
|
"@types/node"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@inquirer/core@10.1.7_@types+node@22.13.8": {
|
"@inquirer/core@10.1.7_@types+node@22.13.8": {
|
||||||
@@ -1107,7 +1016,7 @@
|
|||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@inquirer/figures",
|
"@inquirer/figures",
|
||||||
"@inquirer/type",
|
"@inquirer/type",
|
||||||
"@types/node@22.13.8",
|
"@types/node",
|
||||||
"ansi-escapes",
|
"ansi-escapes",
|
||||||
"cli-width",
|
"cli-width",
|
||||||
"mute-stream",
|
"mute-stream",
|
||||||
@@ -1122,7 +1031,7 @@
|
|||||||
"@inquirer/type@3.0.4_@types+node@22.13.8": {
|
"@inquirer/type@3.0.4_@types+node@22.13.8": {
|
||||||
"integrity": "sha512-2MNFrDY8jkFYc9Il9DgLsHhMzuHnOYM1+CUYVWbzu9oT0hC7V7EcYvdCKeoll/Fcci04A+ERZ9wcc7cQ8lTkIA==",
|
"integrity": "sha512-2MNFrDY8jkFYc9Il9DgLsHhMzuHnOYM1+CUYVWbzu9oT0hC7V7EcYvdCKeoll/Fcci04A+ERZ9wcc7cQ8lTkIA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@types/node@22.13.8"
|
"@types/node"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@isaacs/cliui@8.0.2": {
|
"@isaacs/cliui@8.0.2": {
|
||||||
@@ -1305,29 +1214,12 @@
|
|||||||
"@open-draft/until@2.1.0": {
|
"@open-draft/until@2.1.0": {
|
||||||
"integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="
|
"integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="
|
||||||
},
|
},
|
||||||
"@pivanov/utils@0.0.1_react@19.0.0_react-dom@19.0.0__react@19.0.0": {
|
|
||||||
"integrity": "sha512-JQ/pXeG9/Yq3UuwH2Xp4F6bSAIDGzbxT0Vrg/82tMi3Yp+Ps9AYzjSDE+zfvBRqc7J11V6MMonUrWj4+2dYgrg==",
|
|
||||||
"dependencies": [
|
|
||||||
"react",
|
|
||||||
"react-dom"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"@pkgjs/parseargs@0.11.0": {
|
"@pkgjs/parseargs@0.11.0": {
|
||||||
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="
|
"integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="
|
||||||
},
|
},
|
||||||
"@polka/url@1.0.0-next.28": {
|
"@polka/url@1.0.0-next.28": {
|
||||||
"integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw=="
|
"integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw=="
|
||||||
},
|
},
|
||||||
"@preact/signals-core@1.8.0": {
|
|
||||||
"integrity": "sha512-OBvUsRZqNmjzCZXWLxkZfhcgT+Fk8DDcT/8vD6a1xhDemodyy87UJRJfASMuSD8FaAIeGgGm85ydXhm7lr4fyA=="
|
|
||||||
},
|
|
||||||
"@preact/signals@1.3.2_preact@10.26.4": {
|
|
||||||
"integrity": "sha512-naxcJgUJ6BTOROJ7C3QML7KvwKwCXQJYTc5L/b0eEsdYgPB6SxwoQ1vDGcS0Q7GVjAenVq/tXrybVdFShHYZWg==",
|
|
||||||
"dependencies": [
|
|
||||||
"@preact/signals-core",
|
|
||||||
"preact"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"@radix-ui/number@1.1.0": {
|
"@radix-ui/number@1.1.0": {
|
||||||
"integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ=="
|
"integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ=="
|
||||||
},
|
},
|
||||||
@@ -3531,16 +3423,10 @@
|
|||||||
"@types/pbf"
|
"@types/pbf"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@types/node@20.17.22": {
|
|
||||||
"integrity": "sha512-9RV2zST+0s3EhfrMZIhrz2bhuhBwxgkbHEwP2gtGWPjBzVQjifMzJ9exw7aDZhR1wbpj8zBrfp3bo8oJcGiUUw==",
|
|
||||||
"dependencies": [
|
|
||||||
"undici-types@6.19.8"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"@types/node@22.13.8": {
|
"@types/node@22.13.8": {
|
||||||
"integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==",
|
"integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"undici-types@6.20.0"
|
"undici-types"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"@types/pbf@3.0.5": {
|
"@types/pbf@3.0.5": {
|
||||||
@@ -3848,9 +3734,6 @@
|
|||||||
"bignumber.js@9.1.2": {
|
"bignumber.js@9.1.2": {
|
||||||
"integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug=="
|
"integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug=="
|
||||||
},
|
},
|
||||||
"bippy@0.2.7": {
|
|
||||||
"integrity": "sha512-LTCos3SmOJHrag0qF91tLUZMMw6wA+i15ESRBp71pvfNlTMYcxYoJHJ/pvFhd+29Wm5vfgVxBHV7kP5OKUUipg=="
|
|
||||||
},
|
|
||||||
"bn.js@4.12.1": {
|
"bn.js@4.12.1": {
|
||||||
"integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg=="
|
"integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg=="
|
||||||
},
|
},
|
||||||
@@ -4458,64 +4341,34 @@
|
|||||||
"is-symbol"
|
"is-symbol"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"esbuild@0.24.2": {
|
|
||||||
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
|
|
||||||
"dependencies": [
|
|
||||||
"@esbuild/aix-ppc64@0.24.2",
|
|
||||||
"@esbuild/android-arm@0.24.2",
|
|
||||||
"@esbuild/android-arm64@0.24.2",
|
|
||||||
"@esbuild/android-x64@0.24.2",
|
|
||||||
"@esbuild/darwin-arm64@0.24.2",
|
|
||||||
"@esbuild/darwin-x64@0.24.2",
|
|
||||||
"@esbuild/freebsd-arm64@0.24.2",
|
|
||||||
"@esbuild/freebsd-x64@0.24.2",
|
|
||||||
"@esbuild/linux-arm@0.24.2",
|
|
||||||
"@esbuild/linux-arm64@0.24.2",
|
|
||||||
"@esbuild/linux-ia32@0.24.2",
|
|
||||||
"@esbuild/linux-loong64@0.24.2",
|
|
||||||
"@esbuild/linux-mips64el@0.24.2",
|
|
||||||
"@esbuild/linux-ppc64@0.24.2",
|
|
||||||
"@esbuild/linux-riscv64@0.24.2",
|
|
||||||
"@esbuild/linux-s390x@0.24.2",
|
|
||||||
"@esbuild/linux-x64@0.24.2",
|
|
||||||
"@esbuild/netbsd-arm64@0.24.2",
|
|
||||||
"@esbuild/netbsd-x64@0.24.2",
|
|
||||||
"@esbuild/openbsd-arm64@0.24.2",
|
|
||||||
"@esbuild/openbsd-x64@0.24.2",
|
|
||||||
"@esbuild/sunos-x64@0.24.2",
|
|
||||||
"@esbuild/win32-arm64@0.24.2",
|
|
||||||
"@esbuild/win32-ia32@0.24.2",
|
|
||||||
"@esbuild/win32-x64@0.24.2"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"esbuild@0.25.0": {
|
"esbuild@0.25.0": {
|
||||||
"integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
|
"integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@esbuild/aix-ppc64@0.25.0",
|
"@esbuild/aix-ppc64",
|
||||||
"@esbuild/android-arm@0.25.0",
|
"@esbuild/android-arm",
|
||||||
"@esbuild/android-arm64@0.25.0",
|
"@esbuild/android-arm64",
|
||||||
"@esbuild/android-x64@0.25.0",
|
"@esbuild/android-x64",
|
||||||
"@esbuild/darwin-arm64@0.25.0",
|
"@esbuild/darwin-arm64",
|
||||||
"@esbuild/darwin-x64@0.25.0",
|
"@esbuild/darwin-x64",
|
||||||
"@esbuild/freebsd-arm64@0.25.0",
|
"@esbuild/freebsd-arm64",
|
||||||
"@esbuild/freebsd-x64@0.25.0",
|
"@esbuild/freebsd-x64",
|
||||||
"@esbuild/linux-arm@0.25.0",
|
"@esbuild/linux-arm",
|
||||||
"@esbuild/linux-arm64@0.25.0",
|
"@esbuild/linux-arm64",
|
||||||
"@esbuild/linux-ia32@0.25.0",
|
"@esbuild/linux-ia32",
|
||||||
"@esbuild/linux-loong64@0.25.0",
|
"@esbuild/linux-loong64",
|
||||||
"@esbuild/linux-mips64el@0.25.0",
|
"@esbuild/linux-mips64el",
|
||||||
"@esbuild/linux-ppc64@0.25.0",
|
"@esbuild/linux-ppc64",
|
||||||
"@esbuild/linux-riscv64@0.25.0",
|
"@esbuild/linux-riscv64",
|
||||||
"@esbuild/linux-s390x@0.25.0",
|
"@esbuild/linux-s390x",
|
||||||
"@esbuild/linux-x64@0.25.0",
|
"@esbuild/linux-x64",
|
||||||
"@esbuild/netbsd-arm64@0.25.0",
|
"@esbuild/netbsd-arm64",
|
||||||
"@esbuild/netbsd-x64@0.25.0",
|
"@esbuild/netbsd-x64",
|
||||||
"@esbuild/openbsd-arm64@0.25.0",
|
"@esbuild/openbsd-arm64",
|
||||||
"@esbuild/openbsd-x64@0.25.0",
|
"@esbuild/openbsd-x64",
|
||||||
"@esbuild/sunos-x64@0.25.0",
|
"@esbuild/sunos-x64",
|
||||||
"@esbuild/win32-arm64@0.25.0",
|
"@esbuild/win32-arm64",
|
||||||
"@esbuild/win32-ia32@0.25.0",
|
"@esbuild/win32-ia32",
|
||||||
"@esbuild/win32-x64@0.25.0"
|
"@esbuild/win32-x64"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"escalade@3.2.0": {
|
"escalade@3.2.0": {
|
||||||
@@ -4701,12 +4554,6 @@
|
|||||||
"get-intrinsic"
|
"get-intrinsic"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"get-tsconfig@4.10.0": {
|
|
||||||
"integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==",
|
|
||||||
"dependencies": [
|
|
||||||
"resolve-pkg-maps"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"get-value@2.0.6": {
|
"get-value@2.0.6": {
|
||||||
"integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA=="
|
"integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA=="
|
||||||
},
|
},
|
||||||
@@ -5139,9 +4986,6 @@
|
|||||||
"kind-of@6.0.3": {
|
"kind-of@6.0.3": {
|
||||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
|
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="
|
||||||
},
|
},
|
||||||
"kleur@4.1.5": {
|
|
||||||
"integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="
|
|
||||||
},
|
|
||||||
"leven@3.1.0": {
|
"leven@3.1.0": {
|
||||||
"integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="
|
"integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="
|
||||||
},
|
},
|
||||||
@@ -5340,9 +5184,6 @@
|
|||||||
"mkdirp@3.0.1": {
|
"mkdirp@3.0.1": {
|
||||||
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="
|
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="
|
||||||
},
|
},
|
||||||
"mri@1.2.0": {
|
|
||||||
"integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="
|
|
||||||
},
|
|
||||||
"mrmime@2.0.1": {
|
"mrmime@2.0.1": {
|
||||||
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="
|
"integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ=="
|
||||||
},
|
},
|
||||||
@@ -5607,9 +5448,6 @@
|
|||||||
"potpack@2.0.0": {
|
"potpack@2.0.0": {
|
||||||
"integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw=="
|
"integrity": "sha512-Q+/tYsFU9r7xoOJ+y/ZTtdVQwTWfzjbiXBDMM/JKUux3+QPP02iUuIoeBQ+Ot6oEDlC+/PGjB/5A3K7KKb7hcw=="
|
||||||
},
|
},
|
||||||
"preact@10.26.4": {
|
|
||||||
"integrity": "sha512-KJhO7LBFTjP71d83trW+Ilnjbo+ySsaAgCfXOXUlmGzJ4ygYPWmysm77yg4emwfmoz3b22yvH5IsVFHbhUaH5w=="
|
|
||||||
},
|
|
||||||
"pretty-bytes@5.6.0": {
|
"pretty-bytes@5.6.0": {
|
||||||
"integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="
|
"integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg=="
|
||||||
},
|
},
|
||||||
@@ -5777,31 +5615,6 @@
|
|||||||
"use-sidecar"
|
"use-sidecar"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"react-scan@0.2.8_react@19.0.0_react-dom@19.0.0__react@19.0.0_preact@10.26.4": {
|
|
||||||
"integrity": "sha512-+6Gvu9b0UMmzV0JkigA7Y2YcjQABiNrweP9l9j8nrutN5OAYLRe4JgfwiUohPFngMD+Y6I5N0kW+okXhvVLGUw==",
|
|
||||||
"dependencies": [
|
|
||||||
"@babel/core",
|
|
||||||
"@babel/generator",
|
|
||||||
"@babel/types",
|
|
||||||
"@clack/core",
|
|
||||||
"@clack/prompts",
|
|
||||||
"@pivanov/utils",
|
|
||||||
"@preact/signals",
|
|
||||||
"@rollup/pluginutils@5.1.4_rollup@2.79.2",
|
|
||||||
"@types/node@20.17.22",
|
|
||||||
"bippy",
|
|
||||||
"esbuild@0.24.2",
|
|
||||||
"estree-walker@3.0.3",
|
|
||||||
"kleur",
|
|
||||||
"mri",
|
|
||||||
"playwright",
|
|
||||||
"preact",
|
|
||||||
"react",
|
|
||||||
"react-dom",
|
|
||||||
"tsx",
|
|
||||||
"unplugin"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"react-style-singleton@2.2.3_@types+react@19.0.10_react@19.0.0": {
|
"react-style-singleton@2.2.3_@types+react@19.0.10_react@19.0.0": {
|
||||||
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
|
"integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -5912,9 +5725,6 @@
|
|||||||
"requires-port@1.0.0": {
|
"requires-port@1.0.0": {
|
||||||
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
|
"integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ=="
|
||||||
},
|
},
|
||||||
"resolve-pkg-maps@1.0.0": {
|
|
||||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="
|
|
||||||
},
|
|
||||||
"resolve-protobuf-schema@2.1.0": {
|
"resolve-protobuf-schema@2.1.0": {
|
||||||
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
|
"integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
@@ -6153,9 +5963,6 @@
|
|||||||
"totalist"
|
"totalist"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"sisteransi@1.0.5": {
|
|
||||||
"integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg=="
|
|
||||||
},
|
|
||||||
"skmeans@0.9.7": {
|
"skmeans@0.9.7": {
|
||||||
"integrity": "sha512-hNj1/oZ7ygsfmPZ7ZfN5MUBRoGg1gtpnImuJBgLO0ljQ67DtJuiQaiYdS4lUA6s0KCwnPhGivtC/WRwIZLkHyg=="
|
"integrity": "sha512-hNj1/oZ7ygsfmPZ7ZfN5MUBRoGg1gtpnImuJBgLO0ljQ67DtJuiQaiYdS4lUA6s0KCwnPhGivtC/WRwIZLkHyg=="
|
||||||
},
|
},
|
||||||
@@ -6516,14 +6323,6 @@
|
|||||||
"tslog@4.9.3": {
|
"tslog@4.9.3": {
|
||||||
"integrity": "sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw=="
|
"integrity": "sha512-oDWuGVONxhVEBtschLf2cs/Jy8i7h1T+CpdkTNWQgdAF7DhRo2G8vMCgILKe7ojdEkLhICWgI1LYSSKaJsRgcw=="
|
||||||
},
|
},
|
||||||
"tsx@4.19.3": {
|
|
||||||
"integrity": "sha512-4H8vUNGNjQ4V2EOoGw005+c+dGuPSnhpPBPHBtsZdGZBk/iJb4kguGlPWaZTZ3q5nMtFOEsY0nRDlh9PJyd6SQ==",
|
|
||||||
"dependencies": [
|
|
||||||
"esbuild@0.25.0",
|
|
||||||
"fsevents@2.3.3",
|
|
||||||
"get-tsconfig"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"tty-browserify@0.0.1": {
|
"tty-browserify@0.0.1": {
|
||||||
"integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw=="
|
"integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw=="
|
||||||
},
|
},
|
||||||
@@ -6601,9 +6400,6 @@
|
|||||||
"which-boxed-primitive"
|
"which-boxed-primitive"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"undici-types@6.19.8": {
|
|
||||||
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw=="
|
|
||||||
},
|
|
||||||
"undici-types@6.20.0": {
|
"undici-types@6.20.0": {
|
||||||
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
|
"integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="
|
||||||
},
|
},
|
||||||
@@ -6644,13 +6440,6 @@
|
|||||||
"universalify@2.0.1": {
|
"universalify@2.0.1": {
|
||||||
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="
|
"integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="
|
||||||
},
|
},
|
||||||
"unplugin@2.1.0": {
|
|
||||||
"integrity": "sha512-us4j03/499KhbGP8BU7Hrzrgseo+KdfJYWcbcajCOqsAyb8Gk0Yn2kiUIcZISYCb1JFaZfIuG3b42HmguVOKCQ==",
|
|
||||||
"dependencies": [
|
|
||||||
"acorn",
|
|
||||||
"webpack-virtual-modules"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"upath@1.2.0": {
|
"upath@1.2.0": {
|
||||||
"integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg=="
|
"integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg=="
|
||||||
},
|
},
|
||||||
@@ -6747,8 +6536,8 @@
|
|||||||
"vite@6.2.0_@types+node@22.13.8": {
|
"vite@6.2.0_@types+node@22.13.8": {
|
||||||
"integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==",
|
"integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@types/node@22.13.8",
|
"@types/node",
|
||||||
"esbuild@0.25.0",
|
"esbuild",
|
||||||
"fsevents@2.3.3",
|
"fsevents@2.3.3",
|
||||||
"postcss",
|
"postcss",
|
||||||
"rollup@4.34.9"
|
"rollup@4.34.9"
|
||||||
@@ -6757,7 +6546,7 @@
|
|||||||
"vitest@3.0.8_@types+node@22.13.8_happy-dom@17.2.2_vite@6.2.0__@types+node@22.13.8_@vitest+browser@3.0.8__playwright@1.50.1__vitest@3.0.8___@types+node@22.13.8___happy-dom@17.2.2___@vitest+browser@3.0.8____playwright@1.50.1____vitest@3.0.8____msw@2.7.3_____typescript@5.8.2_____@types+node@22.13.8____vite@6.2.0_____@types+node@22.13.8____typescript@5.8.2____@types+node@22.13.8____happy-dom@17.2.2___playwright@1.50.1___vite@6.2.0____@types+node@22.13.8___typescript@5.8.2__vitest@3.0.8__typescript@5.8.2__msw@2.7.3___typescript@5.8.2___@types+node@22.13.8__vite@6.2.0___@types+node@22.13.8__@types+node@22.13.8_playwright@1.50.1_typescript@5.8.2": {
|
"vitest@3.0.8_@types+node@22.13.8_happy-dom@17.2.2_vite@6.2.0__@types+node@22.13.8_@vitest+browser@3.0.8__playwright@1.50.1__vitest@3.0.8___@types+node@22.13.8___happy-dom@17.2.2___@vitest+browser@3.0.8____playwright@1.50.1____vitest@3.0.8____msw@2.7.3_____typescript@5.8.2_____@types+node@22.13.8____vite@6.2.0_____@types+node@22.13.8____typescript@5.8.2____@types+node@22.13.8____happy-dom@17.2.2___playwright@1.50.1___vite@6.2.0____@types+node@22.13.8___typescript@5.8.2__vitest@3.0.8__typescript@5.8.2__msw@2.7.3___typescript@5.8.2___@types+node@22.13.8__vite@6.2.0___@types+node@22.13.8__@types+node@22.13.8_playwright@1.50.1_typescript@5.8.2": {
|
||||||
"integrity": "sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA==",
|
"integrity": "sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA==",
|
||||||
"dependencies": [
|
"dependencies": [
|
||||||
"@types/node@22.13.8",
|
"@types/node",
|
||||||
"@vitest/browser",
|
"@vitest/browser",
|
||||||
"@vitest/expect",
|
"@vitest/expect",
|
||||||
"@vitest/mocker@3.0.8_vite@6.2.0__@types+node@22.13.8_@types+node@22.13.8_msw@2.7.3__typescript@5.8.2__@types+node@22.13.8_typescript@5.8.2",
|
"@vitest/mocker@3.0.8_vite@6.2.0__@types+node@22.13.8_@types+node@22.13.8_msw@2.7.3__typescript@5.8.2__@types+node@22.13.8_typescript@5.8.2",
|
||||||
@@ -6799,9 +6588,6 @@
|
|||||||
"webidl-conversions@7.0.0": {
|
"webidl-conversions@7.0.0": {
|
||||||
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
|
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g=="
|
||||||
},
|
},
|
||||||
"webpack-virtual-modules@0.6.2": {
|
|
||||||
"integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ=="
|
|
||||||
},
|
|
||||||
"whatwg-mimetype@3.0.0": {
|
"whatwg-mimetype@3.0.0": {
|
||||||
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="
|
"integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q=="
|
||||||
},
|
},
|
||||||
@@ -7151,7 +6937,6 @@
|
|||||||
"npm:react-hook-form@^7.54.2",
|
"npm:react-hook-form@^7.54.2",
|
||||||
"npm:react-map-gl@8.0.1",
|
"npm:react-map-gl@8.0.1",
|
||||||
"npm:react-qrcode-logo@3",
|
"npm:react-qrcode-logo@3",
|
||||||
"npm:react-scan@~0.2.8",
|
|
||||||
"npm:react@19",
|
"npm:react@19",
|
||||||
"npm:rfc4648@^1.5.4",
|
"npm:rfc4648@^1.5.4",
|
||||||
"npm:simple-git-hooks@^2.11.1",
|
"npm:simple-git-hooks@^2.11.1",
|
||||||
|
|||||||
@@ -12,7 +12,6 @@
|
|||||||
"format": "deno fmt src/",
|
"format": "deno fmt src/",
|
||||||
"dev": "deno task dev:ui",
|
"dev": "deno task dev:ui",
|
||||||
"dev:ui": "deno run -A npm:vite dev",
|
"dev:ui": "deno run -A npm:vite dev",
|
||||||
"dev:scan": "VITE_DEBUG_SCAN=true deno task dev:ui",
|
|
||||||
"test": "deno run -A npm:vitest",
|
"test": "deno run -A npm:vitest",
|
||||||
"preview": "deno run -A npm:vite preview",
|
"preview": "deno run -A npm:vite preview",
|
||||||
"package": "gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ ."
|
"package": "gzipper c -i html,js,css,png,ico,svg,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ ."
|
||||||
@@ -72,10 +71,8 @@
|
|||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-map-gl": "8.0.1",
|
"react-map-gl": "8.0.1",
|
||||||
"react-qrcode-logo": "^3.0.0",
|
"react-qrcode-logo": "^3.0.0",
|
||||||
"react-scan": "^0.2.8",
|
|
||||||
"rfc4648": "^1.5.4",
|
"rfc4648": "^1.5.4",
|
||||||
"vite-plugin-node-polyfills": "^0.23.0",
|
"vite-plugin-node-polyfills": "^0.23.0",
|
||||||
|
|
||||||
"zustand": "5.0.3"
|
"zustand": "5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@@ -6,8 +6,9 @@ import { PkiBackupDialog } from "@components/Dialog/PKIBackupDialog.tsx";
|
|||||||
import { QRDialog } from "@components/Dialog/QRDialog.tsx";
|
import { QRDialog } from "@components/Dialog/QRDialog.tsx";
|
||||||
import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
|
import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
|
||||||
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
|
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
|
||||||
import { NodeDetailsDialog } from "@components/Dialog/NodeDetailsDialog.tsx";
|
import { NodeDetailsDialog } from "@components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx";
|
||||||
import { UnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx";
|
import { UnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx";
|
||||||
|
import { RefreshKeysDialog } from "@components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx";
|
||||||
|
|
||||||
export const DialogManager = () => {
|
export const DialogManager = () => {
|
||||||
const { channels, config, dialog, setDialogOpen } = useDevice();
|
const { channels, config, dialog, setDialogOpen } = useDevice();
|
||||||
@@ -70,6 +71,12 @@ export const DialogManager = () => {
|
|||||||
setDialogOpen("unsafeRoles", open);
|
setDialogOpen("unsafeRoles", open);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<RefreshKeysDialog
|
||||||
|
open={dialog.refreshKeys}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setDialogOpen("refreshKeys", open);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ const links: { [key: string]: string } = {
|
|||||||
|
|
||||||
const listFormatter = new Intl.ListFormat("en", {
|
const listFormatter = new Intl.ListFormat("en", {
|
||||||
style: "long",
|
style: "long",
|
||||||
type: "conjunction",
|
type: "disjunction",
|
||||||
});
|
});
|
||||||
|
|
||||||
const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => {
|
const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => {
|
||||||
@@ -79,16 +79,16 @@ const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Subtle className="flex flex-col items-start gap-2 text-slate-900 bg-red-200/80 p-4 rounded-md">
|
<Subtle className="flex flex-col items-start gap-2 bg-red-500 p-4 rounded-md">
|
||||||
<div className="flex items-center gap-2 w-full">
|
<div className="flex items-center gap-2 w-full">
|
||||||
<AlertCircle size={40} className="mr-2 shrink-0" />
|
<AlertCircle size={40} className="mr-2 shrink-0 text-white" />
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
<p className="text-sm">
|
<p className="text-sm text-white">
|
||||||
{browserFeatures.length > 0 && (
|
{browserFeatures.length > 0 && (
|
||||||
<>
|
<>
|
||||||
This application requires{" "}
|
This connection type requires{" "}
|
||||||
{formatFeatureList(browserFeatures)}. Please use a
|
{formatFeatureList(browserFeatures)}. Please use a
|
||||||
Chromium-based browser like Chrome or Edge.
|
supported browser, like Chrome or Edge.
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{needsSecureContext && (
|
{needsSecureContext && (
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
import { useAppStore } from "../../core/stores/appStore.ts";
|
|
||||||
import { useDevice } from "../../core/stores/deviceStore.ts";
|
|
||||||
import {
|
|
||||||
Accordion,
|
|
||||||
AccordionContent,
|
|
||||||
AccordionItem,
|
|
||||||
AccordionTrigger,
|
|
||||||
} from "../UI/Accordion.tsx";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "../UI/Dialog.tsx";
|
|
||||||
import { Protobuf } from "@meshtastic/core";
|
|
||||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
|
||||||
import { DeviceImage } from "../generic/DeviceImage.tsx";
|
|
||||||
import { TimeAgo } from "../generic/TimeAgo.tsx";
|
|
||||||
import { Uptime } from "../generic/Uptime.tsx";
|
|
||||||
|
|
||||||
export interface NodeDetailsDialogProps {
|
|
||||||
open: boolean;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const NodeDetailsDialog = ({
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
}: NodeDetailsDialogProps) => {
|
|
||||||
const { nodes } = useDevice();
|
|
||||||
const { nodeNumDetails } = useAppStore();
|
|
||||||
const device: Protobuf.Mesh.NodeInfo = nodes.get(nodeNumDetails);
|
|
||||||
|
|
||||||
return device
|
|
||||||
? (
|
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogClose />
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>
|
|
||||||
Node Details for {device.user?.longName ?? "UNKNOWN"} (
|
|
||||||
{device.user?.shortName ?? "UNK"})
|
|
||||||
</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<DialogFooter>
|
|
||||||
<div className="w-full">
|
|
||||||
<DeviceImage
|
|
||||||
className="w-32 h-32 mx-auto rounded-lg border-4 border-slate-200 dark:border-slate-800"
|
|
||||||
deviceType={Protobuf.Mesh
|
|
||||||
.HardwareModel[device.user?.hwModel ?? 0]}
|
|
||||||
/>
|
|
||||||
<div className="bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
|
||||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
|
||||||
Details:
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Hardware:{" "}
|
|
||||||
{Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]}
|
|
||||||
</p>
|
|
||||||
<p>Node Number: {device.num}</p>
|
|
||||||
<p>Node HEX: !{numberToHexUnpadded(device.num)}</p>
|
|
||||||
<p>
|
|
||||||
Role: {Protobuf.Config.Config_DeviceConfig_Role[
|
|
||||||
device.user?.role ?? 0
|
|
||||||
]}
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Last Heard: {device.lastHeard === 0
|
|
||||||
? (
|
|
||||||
"Never"
|
|
||||||
)
|
|
||||||
: <TimeAgo timestamp={device.lastHeard * 1000} />}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{device.position
|
|
||||||
? (
|
|
||||||
<div className="mt-5 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
|
||||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
|
||||||
Position:
|
|
||||||
</p>
|
|
||||||
{device.position.latitudeI && device.position.longitudeI
|
|
||||||
? (
|
|
||||||
<p>
|
|
||||||
Coordinates:{" "}
|
|
||||||
<a
|
|
||||||
className="text-blue-500 dark:text-blue-400"
|
|
||||||
href={`https://www.openstreetmap.org/?mlat=${device.position.latitudeI / 1e7
|
|
||||||
}&mlon=${device.position.longitudeI / 1e7
|
|
||||||
}&layers=N`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
{device.position.latitudeI / 1e7},{" "}
|
|
||||||
{device.position.longitudeI / 1e7}
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
{device.position.altitude
|
|
||||||
? <p>Altitude: {device.position.altitude}m</p>
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
|
|
||||||
{device.deviceMetrics
|
|
||||||
? (
|
|
||||||
<div className="mt-5 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
|
||||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
|
||||||
Device Metrics:
|
|
||||||
</p>
|
|
||||||
{device.deviceMetrics.airUtilTx
|
|
||||||
? (
|
|
||||||
<p>
|
|
||||||
Air TX utilization:{" "}
|
|
||||||
{device.deviceMetrics.airUtilTx.toFixed(2)}%
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
{device.deviceMetrics.channelUtilization
|
|
||||||
? (
|
|
||||||
<p>
|
|
||||||
Channel utilization:{" "}
|
|
||||||
{device.deviceMetrics.channelUtilization.toFixed(2)}%
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
{device.deviceMetrics.batteryLevel
|
|
||||||
? (
|
|
||||||
<p>
|
|
||||||
Battery level:{" "}
|
|
||||||
{device.deviceMetrics.batteryLevel.toFixed(2)}%
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
{device.deviceMetrics.voltage
|
|
||||||
? (
|
|
||||||
<p>
|
|
||||||
Voltage: {device.deviceMetrics.voltage.toFixed(2)}V
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
{device.deviceMetrics.uptimeSeconds
|
|
||||||
? (
|
|
||||||
<p>
|
|
||||||
Uptime:{" "}
|
|
||||||
<Uptime
|
|
||||||
seconds={device.deviceMetrics.uptimeSeconds}
|
|
||||||
/>
|
|
||||||
</p>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
|
|
||||||
{device
|
|
||||||
? (
|
|
||||||
<div className="mt-5 w-full max-w-[464px] bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
|
||||||
<Accordion
|
|
||||||
className="AccordionRoot"
|
|
||||||
type="single"
|
|
||||||
collapsible
|
|
||||||
>
|
|
||||||
<AccordionItem className="AccordionItem" value="item-1">
|
|
||||||
<AccordionTrigger>
|
|
||||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
|
||||||
All Raw Metrics:
|
|
||||||
</p>
|
|
||||||
</AccordionTrigger>
|
|
||||||
<AccordionContent className="overflow-x-scroll">
|
|
||||||
<pre className="text-xs w-full">
|
|
||||||
{JSON.stringify(device, null, 2)}
|
|
||||||
</pre>
|
|
||||||
</AccordionContent>
|
|
||||||
</AccordionItem>
|
|
||||||
</Accordion>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
</div>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
: null;
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import { describe, it, vi, expect, beforeEach, Mock } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import { NodeDetailsDialog } from "@components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx";
|
||||||
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
|
import { useAppStore } from "@core/stores/appStore.ts";
|
||||||
|
|
||||||
|
vi.mock("@core/stores/deviceStore");
|
||||||
|
vi.mock("@core/stores/appStore");
|
||||||
|
|
||||||
|
describe("NodeDetailsDialog", () => {
|
||||||
|
const mockDevice = {
|
||||||
|
num: 1234,
|
||||||
|
user: {
|
||||||
|
longName: "Test Node",
|
||||||
|
shortName: "TN",
|
||||||
|
hwModel: 1,
|
||||||
|
role: 1,
|
||||||
|
},
|
||||||
|
lastHeard: 1697500000,
|
||||||
|
position: {
|
||||||
|
latitudeI: 450000000,
|
||||||
|
longitudeI: -750000000,
|
||||||
|
altitude: 200,
|
||||||
|
},
|
||||||
|
deviceMetrics: {
|
||||||
|
airUtilTx: 50.123,
|
||||||
|
channelUtilization: 75.456,
|
||||||
|
batteryLevel: 88.789,
|
||||||
|
voltage: 4.2,
|
||||||
|
uptimeSeconds: 3600,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Reset mocks before each test
|
||||||
|
vi.resetAllMocks();
|
||||||
|
|
||||||
|
(useDevice as Mock).mockReturnValue({
|
||||||
|
nodes: new Map([[1234, mockDevice]]),
|
||||||
|
});
|
||||||
|
|
||||||
|
(useAppStore as unknown as Mock).mockReturnValue({
|
||||||
|
nodeNumDetails: 1234,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders node details correctly", () => {
|
||||||
|
render(<NodeDetailsDialog open={true} onOpenChange={() => { }} />);
|
||||||
|
|
||||||
|
expect(screen.getByText(/Node Details for Test Node/i)).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText("Node Number: 1234")).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText(/Air TX utilization: 50.12%/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Channel utilization: 75.46%/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Battery level: 88.79%/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Voltage: 4.20V/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Uptime:/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Coordinates:/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("45, -75")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Altitude: 200m/i)).toBeInTheDocument();
|
||||||
|
expect(screen.getByText(/Role:/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders null if device is not found", () => {
|
||||||
|
(useDevice as Mock).mockReturnValue({
|
||||||
|
nodes: new Map(),
|
||||||
|
});
|
||||||
|
|
||||||
|
render(<NodeDetailsDialog open={true} onOpenChange={() => { }} />);
|
||||||
|
expect(screen.queryByText(/Node Details for/i)).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,177 @@
|
|||||||
|
import { useAppStore } from "@core/stores/appStore.ts";
|
||||||
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
|
import {
|
||||||
|
Accordion,
|
||||||
|
AccordionContent,
|
||||||
|
AccordionItem,
|
||||||
|
AccordionTrigger,
|
||||||
|
} from "@components/UI/Accordion.tsx";
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@components/UI/Dialog.tsx";
|
||||||
|
import { Protobuf } from "@meshtastic/core";
|
||||||
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
|
import { DeviceImage } from "@components/generic/DeviceImage.tsx";
|
||||||
|
import { TimeAgo } from "@components/generic/TimeAgo.tsx";
|
||||||
|
import { Uptime } from "@components/generic/Uptime.tsx";
|
||||||
|
|
||||||
|
export interface NodeDetailsDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const NodeDetailsDialog = ({
|
||||||
|
open,
|
||||||
|
onOpenChange,
|
||||||
|
}: NodeDetailsDialogProps) => {
|
||||||
|
const { nodes } = useDevice();
|
||||||
|
const { nodeNumDetails } = useAppStore();
|
||||||
|
|
||||||
|
const device = nodes.get(nodeNumDetails);
|
||||||
|
|
||||||
|
if (!device) return null;
|
||||||
|
|
||||||
|
const deviceMetricsMap = [
|
||||||
|
{
|
||||||
|
key: "airUtilTx",
|
||||||
|
label: "Air TX utilization",
|
||||||
|
value: device.deviceMetrics?.airUtilTx,
|
||||||
|
format: (val: number) => `${val.toFixed(2)}%`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "channelUtilization",
|
||||||
|
label: "Channel utilization",
|
||||||
|
value: device.deviceMetrics?.channelUtilization,
|
||||||
|
format: (val: number) => `${val.toFixed(2)}%`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "batteryLevel",
|
||||||
|
label: "Battery level",
|
||||||
|
value: device.deviceMetrics?.batteryLevel,
|
||||||
|
format: (val: number) => `${val.toFixed(2)}%`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "voltage",
|
||||||
|
label: "Voltage",
|
||||||
|
value: device.deviceMetrics?.voltage,
|
||||||
|
format: (val: number) => `${val.toFixed(2)}V`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent >
|
||||||
|
<DialogClose />
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>
|
||||||
|
Node Details for {device.user?.longName ?? "UNKNOWN"} (
|
||||||
|
{device.user?.shortName ?? "UNK"})
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<DialogFooter>
|
||||||
|
<div className="w-full">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<DeviceImage
|
||||||
|
className="w-32 h-32 mx-auto rounded-lg border-4 border-slate-200 dark:border-slate-800"
|
||||||
|
deviceType={
|
||||||
|
Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div className="bg-slate-100 text-slate-900 dark:text-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||||
|
<p className="text-lg font-semibold">Details:</p>
|
||||||
|
<p>
|
||||||
|
Hardware:{" "}
|
||||||
|
{Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]}
|
||||||
|
</p>
|
||||||
|
<p>Node Number: {device.num}</p>
|
||||||
|
<p>Node Hex: !{numberToHexUnpadded(device.num)}</p>
|
||||||
|
<p>
|
||||||
|
Role:{" "}
|
||||||
|
{
|
||||||
|
Protobuf.Config.Config_DeviceConfig_Role[
|
||||||
|
device.user?.role ?? 0
|
||||||
|
]
|
||||||
|
}
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Last Heard:{" "}
|
||||||
|
{device.lastHeard === 0 ? "Never" : <TimeAgo timestamp={device.lastHeard * 1000} />}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{device.position && (
|
||||||
|
<div className="text-slate-900 dark:text-slate-100 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||||
|
<p className="text-lg font-semibold">Position:</p>
|
||||||
|
{device.position.latitudeI && device.position.longitudeI && (
|
||||||
|
<p>
|
||||||
|
Coordinates:{" "}
|
||||||
|
<a
|
||||||
|
className="text-blue-500 dark:text-blue-400"
|
||||||
|
href={`https://www.openstreetmap.org/?mlat=${device.position.latitudeI / 1e7
|
||||||
|
}&mlon=${device.position.longitudeI / 1e7
|
||||||
|
}&layers=N`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
{device.position.latitudeI / 1e7},{" "}
|
||||||
|
{device.position.longitudeI / 1e7}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{device.position.altitude && (
|
||||||
|
<p>Altitude: {device.position.altitude}m</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{device.deviceMetrics && (
|
||||||
|
<div className="text-slate-900 dark:text-slate-100 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||||
|
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
||||||
|
Device Metrics:
|
||||||
|
</p>
|
||||||
|
{deviceMetricsMap.map(
|
||||||
|
(metric) =>
|
||||||
|
metric.value !== undefined && (
|
||||||
|
<p key={metric.key}>
|
||||||
|
{metric.label}: {metric.format(metric.value)}
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
{device.deviceMetrics.uptimeSeconds && (
|
||||||
|
<p>
|
||||||
|
Uptime:{" "}
|
||||||
|
<Uptime seconds={device.deviceMetrics.uptimeSeconds} />
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="text-slate-900 dark:text-slate-100 w-full max-w-[464px] bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
||||||
|
<Accordion className="AccordionRoot" type="single" collapsible>
|
||||||
|
<AccordionItem className="AccordionItem" value="item-1">
|
||||||
|
<AccordionTrigger>
|
||||||
|
<p className="text-lg font-semibold text-slate-900 dark:text-slate-50">
|
||||||
|
All Raw Metrics:
|
||||||
|
</p>
|
||||||
|
</AccordionTrigger>
|
||||||
|
<AccordionContent className="overflow-x-scroll">
|
||||||
|
<pre className="text-xs w-full">
|
||||||
|
{JSON.stringify(device, null, 2)}
|
||||||
|
</pre>
|
||||||
|
</AccordionContent>
|
||||||
|
</AccordionItem>
|
||||||
|
</Accordion>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { render, screen, fireEvent } from "@testing-library/react";
|
||||||
|
import { beforeEach, describe, expect, it, Mock, vi } from "vitest";
|
||||||
|
import { RefreshKeysDialog } from "./RefreshKeysDialog";
|
||||||
|
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
||||||
|
|
||||||
|
vi.mock("./useRefreshKeysDialog.ts", () => ({
|
||||||
|
useRefreshKeysDialog: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("RefreshKeysDialog Component", () => {
|
||||||
|
let handleCloseDialogMock: Mock;
|
||||||
|
let handleNodeRemoveMock: Mock;
|
||||||
|
let onOpenChangeMock: Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
handleCloseDialogMock = vi.fn();
|
||||||
|
handleNodeRemoveMock = vi.fn();
|
||||||
|
onOpenChangeMock = vi.fn();
|
||||||
|
|
||||||
|
(useRefreshKeysDialog as Mock).mockReturnValue({
|
||||||
|
handleCloseDialog: handleCloseDialogMock,
|
||||||
|
handleNodeRemove: handleNodeRemoveMock,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the dialog with correct content", () => {
|
||||||
|
render(<RefreshKeysDialog open={true} onOpenChange={onOpenChangeMock} />);
|
||||||
|
expect(screen.getByText("Keys Mismatch")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Request New Keys")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Dismiss")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls handleNodeRemove when 'Request New Keys' button is clicked", () => {
|
||||||
|
render(<RefreshKeysDialog open={true} onOpenChange={onOpenChangeMock} />);
|
||||||
|
fireEvent.click(screen.getByText("Request New Keys"));
|
||||||
|
expect(handleNodeRemoveMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls handleCloseDialog when 'Dismiss' button is clicked", () => {
|
||||||
|
render(<RefreshKeysDialog open={true} onOpenChange={onOpenChangeMock} />);
|
||||||
|
fireEvent.click(screen.getByText("Dismiss"));
|
||||||
|
expect(handleCloseDialogMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onOpenChange when dialog close button is clicked", () => {
|
||||||
|
render(<RefreshKeysDialog open={true} onOpenChange={onOpenChangeMock} />);
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /close/i }));
|
||||||
|
expect(handleCloseDialogMock).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render when open is false", () => {
|
||||||
|
render(<RefreshKeysDialog open={false} onOpenChange={onOpenChangeMock} />);
|
||||||
|
expect(screen.queryByText("Keys Mismatch")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@components/UI/Dialog.tsx";
|
||||||
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
|
import { LockKeyholeOpenIcon } from "lucide-react";
|
||||||
|
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
||||||
|
|
||||||
|
export interface RefreshKeysDialogProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RefreshKeysDialog = ({ open, onOpenChange }: RefreshKeysDialogProps) => {
|
||||||
|
|
||||||
|
const { handleCloseDialog, handleNodeRemove } = useRefreshKeysDialog();
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
|
<DialogContent className="max-w-8 flex flex-col gap-2">
|
||||||
|
<DialogClose onClick={handleCloseDialog} />
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Keys Mismatch</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
Your node is unable to send a direct message to this node. This is due to the remote node's current public key not matching the previously stored key for this node.
|
||||||
|
<ul className="mt-2">
|
||||||
|
<li className="flex place-items-center gap-2 items-start">
|
||||||
|
<div className="p-2 bg-slate-500 rounded-lg mt-1">
|
||||||
|
<LockKeyholeOpenIcon size={30} className="text-white justify-center" />
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div>
|
||||||
|
<p className="font-bold mb-0.5">Accept New Keys</p>
|
||||||
|
<p>
|
||||||
|
This will remove the node from device and request new keys.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="default"
|
||||||
|
onClick={handleNodeRemove}
|
||||||
|
className=""
|
||||||
|
>
|
||||||
|
Request New Keys
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={handleCloseDialog}
|
||||||
|
className=""
|
||||||
|
>
|
||||||
|
Dismiss
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
{/* </DialogDescription> */}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog >
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { renderHook, act } from "@testing-library/react";
|
||||||
|
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
||||||
|
import { beforeEach, describe, expect, it, Mock, vi } from "vitest";
|
||||||
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
|
|
||||||
|
vi.mock("@core/stores/appStore.ts", () => ({
|
||||||
|
useAppStore: vi.fn(() => ({ activeChat: "chat-123" })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@core/stores/deviceStore.ts", () => ({
|
||||||
|
useDevice: vi.fn(() => ({
|
||||||
|
removeNode: vi.fn(),
|
||||||
|
setDialogOpen: vi.fn(),
|
||||||
|
getNodeError: vi.fn(),
|
||||||
|
clearNodeError: vi.fn(),
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("useRefreshKeysDialog Hook", () => {
|
||||||
|
let removeNodeMock: Mock;
|
||||||
|
let setDialogOpenMock: Mock;
|
||||||
|
let getNodeErrorMock: Mock;
|
||||||
|
let clearNodeErrorMock: Mock;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
removeNodeMock = vi.fn();
|
||||||
|
setDialogOpenMock = vi.fn();
|
||||||
|
getNodeErrorMock = vi.fn();
|
||||||
|
clearNodeErrorMock = vi.fn();
|
||||||
|
|
||||||
|
(useDevice as Mock).mockReturnValue({
|
||||||
|
removeNode: removeNodeMock,
|
||||||
|
setDialogOpen: setDialogOpenMock,
|
||||||
|
getNodeError: getNodeErrorMock,
|
||||||
|
clearNodeError: clearNodeErrorMock,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handleNodeRemove should remove the node and update dialog if there is an error", () => {
|
||||||
|
getNodeErrorMock.mockReturnValue({ node: "node-abc" });
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useRefreshKeysDialog());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleNodeRemove();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getNodeErrorMock).toHaveBeenCalledWith("chat-123");
|
||||||
|
expect(clearNodeErrorMock).toHaveBeenCalledWith("chat-123");
|
||||||
|
expect(removeNodeMock).toHaveBeenCalledWith("node-abc");
|
||||||
|
expect(setDialogOpenMock).toHaveBeenCalledWith("refreshKeys", false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handleNodeRemove should do nothing if there is no error", () => {
|
||||||
|
getNodeErrorMock.mockReturnValue(undefined);
|
||||||
|
|
||||||
|
const { result } = renderHook(() => useRefreshKeysDialog());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleNodeRemove();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(removeNodeMock).not.toHaveBeenCalled();
|
||||||
|
expect(setDialogOpenMock).not.toHaveBeenCalled();
|
||||||
|
expect(clearNodeErrorMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handleCloseDialog should close the dialog", () => {
|
||||||
|
const { result } = renderHook(() => useRefreshKeysDialog());
|
||||||
|
|
||||||
|
act(() => {
|
||||||
|
result.current.handleCloseDialog();
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(setDialogOpenMock).toHaveBeenCalledWith("refreshKeys", false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import { useCallback } from "react";
|
||||||
|
import { useAppStore } from "@core/stores/appStore.ts";
|
||||||
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
|
|
||||||
|
export function useRefreshKeysDialog() {
|
||||||
|
const { removeNode, setDialogOpen, clearNodeError, getNodeError } = useDevice();
|
||||||
|
const { activeChat } = useAppStore();
|
||||||
|
|
||||||
|
const handleNodeRemove = useCallback(() => {
|
||||||
|
const nodeWithError = getNodeError(activeChat);
|
||||||
|
if (!nodeWithError) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
clearNodeError(activeChat);
|
||||||
|
handleCloseDialog();;
|
||||||
|
return removeNode(nodeWithError?.node);
|
||||||
|
}, [activeChat, clearNodeError, setDialogOpen, removeNode]);
|
||||||
|
|
||||||
|
const handleCloseDialog = useCallback(() => {
|
||||||
|
setDialogOpen('refreshKeys', false);
|
||||||
|
}, [setDialogOpen])
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleCloseDialog,
|
||||||
|
handleNodeRemove
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
@@ -124,7 +124,7 @@ export function DynamicForm<T extends FieldValues>({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{hasSubmitButton && <Button type="submit">Submit</Button>}
|
{hasSubmitButton && <Button type="submit" variant="outline">Submit</Button>}
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,12 +34,8 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
|
|||||||
settings: {
|
settings: {
|
||||||
...data.settings,
|
...data.settings,
|
||||||
psk: toByteArray(pass),
|
psk: toByteArray(pass),
|
||||||
moduleSettings: {
|
moduleSettings: {...data.settings.moduleSettings,
|
||||||
positionPrecision: data.settings.positionEnabled
|
positionPrecision: data.settings.moduleSettings.positionPrecision,
|
||||||
? data.settings.preciseLocation
|
|
||||||
? 32
|
|
||||||
: data.settings.positionPrecision
|
|
||||||
: 0,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -100,17 +96,9 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
|
|||||||
settings: {
|
settings: {
|
||||||
...channel?.settings,
|
...channel?.settings,
|
||||||
psk: pass,
|
psk: pass,
|
||||||
positionEnabled:
|
moduleSettings: {...channel?.settings?.moduleSettings,
|
||||||
channel?.settings?.moduleSettings?.positionPrecision !==
|
positionPrecision: channel?.settings?.moduleSettings?.positionPrecision === undefined ? 10 : channel?.settings?.moduleSettings?.positionPrecision,
|
||||||
undefined &&
|
}
|
||||||
channel?.settings?.moduleSettings?.positionPrecision > 0,
|
|
||||||
preciseLocation:
|
|
||||||
channel?.settings?.moduleSettings?.positionPrecision === 32,
|
|
||||||
positionPrecision:
|
|
||||||
channel?.settings?.moduleSettings?.positionPrecision ===
|
|
||||||
undefined
|
|
||||||
? 10
|
|
||||||
: channel?.settings?.moduleSettings?.positionPrecision,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
@@ -174,39 +162,30 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
|
|||||||
label: "Downlink Enabled",
|
label: "Downlink Enabled",
|
||||||
description: "Send messages from MQTT to the local mesh",
|
description: "Send messages from MQTT to the local mesh",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
type: "toggle",
|
|
||||||
name: "settings.positionEnabled",
|
|
||||||
label: "Allow Position Requests",
|
|
||||||
description: "Send position to channel",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "toggle",
|
|
||||||
name: "settings.preciseLocation",
|
|
||||||
label: "Precise Location",
|
|
||||||
description: "Send precise location to channel",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: "select",
|
type: "select",
|
||||||
name: "settings.positionPrecision",
|
name: "settings.moduleSettings.positionPrecision",
|
||||||
label: "Approximate Location",
|
label: "Location",
|
||||||
description:
|
description:
|
||||||
"If not sharing precise location, position shared on channel will be accurate within this distance",
|
"The precision of the location to share with the channel. Can be disabled.",
|
||||||
properties: {
|
properties: {
|
||||||
enumValue: config.display?.units === 0
|
enumValue: config.display?.units === 0
|
||||||
? {
|
? {
|
||||||
"Within 23 km": 10,
|
"Do not share location": 0,
|
||||||
"Within 12 km": 11,
|
"Within 23 kilometers": 10,
|
||||||
"Within 5.8 km": 12,
|
"Within 12 kilometers": 11,
|
||||||
"Within 2.9 km": 13,
|
"Within 5.8 kilometers": 12,
|
||||||
"Within 1.5 km": 14,
|
"Within 2.9 kilometers": 13,
|
||||||
"Within 700 m": 15,
|
"Within 1.5 kilometers": 14,
|
||||||
"Within 350 m": 16,
|
"Within 700 meters": 15,
|
||||||
"Within 200 m": 17,
|
"Within 350 meters": 16,
|
||||||
"Within 90 m": 18,
|
"Within 200 meters": 17,
|
||||||
"Within 50 m": 19,
|
"Within 90 meters": 18,
|
||||||
|
"Within 50 meters": 19,
|
||||||
|
"Precise Location": 32,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
|
"Do not share location": 0,
|
||||||
"Within 15 miles": 10,
|
"Within 15 miles": 10,
|
||||||
"Within 7.3 miles": 11,
|
"Within 7.3 miles": 11,
|
||||||
"Within 3.6 miles": 12,
|
"Within 3.6 miles": 12,
|
||||||
@@ -217,6 +196,7 @@ export const Channel = ({ channel }: SettingsPanelProps) => {
|
|||||||
"Within 600 feet": 17,
|
"Within 600 feet": 17,
|
||||||
"Within 300 feet": 18,
|
"Within 300 feet": 18,
|
||||||
"Within 150 feet": 19,
|
"Within 150 feet": 19,
|
||||||
|
"Precise Location": 32,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,9 +18,7 @@ export const Serial = ({ closeDialog }: TabElementProps) => {
|
|||||||
setSerialPorts(await navigator?.serial.getPorts());
|
setSerialPorts(await navigator?.serial.getPorts());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
navigator?.serial?.addEventListener("connect", (event) => {
|
navigator?.serial?.addEventListener("connect", () => {
|
||||||
console.log(event);
|
|
||||||
|
|
||||||
updateSerialPortList();
|
updateSerialPortList();
|
||||||
});
|
});
|
||||||
navigator?.serial?.addEventListener("disconnect", () => {
|
navigator?.serial?.addEventListener("disconnect", () => {
|
||||||
@@ -47,8 +45,6 @@ export const Serial = ({ closeDialog }: TabElementProps) => {
|
|||||||
<div className="flex w-full flex-col gap-2 p-4">
|
<div className="flex w-full flex-col gap-2 p-4">
|
||||||
<div className="flex h-48 flex-col gap-2 overflow-y-auto">
|
<div className="flex h-48 flex-col gap-2 overflow-y-auto">
|
||||||
{serialPorts.map((port, index) => {
|
{serialPorts.map((port, index) => {
|
||||||
console.log(port);
|
|
||||||
|
|
||||||
const { usbProductId, usbVendorId } = port.getInfo();
|
const { usbProductId, usbVendorId } = port.getInfo();
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -15,9 +15,10 @@ const EmptyState = () => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
export const ChannelChat = ({
|
export const ChannelChat = ({
|
||||||
messages,
|
messages = [],
|
||||||
}: ChannelChatProps) => {
|
}: ChannelChatProps) => {
|
||||||
const { nodes } = useDevice();
|
const { nodes } = useDevice();
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ export const ChannelChat = ({
|
|||||||
className="flex-1 overflow-y-auto pl-4 pr-4 md:pr-44"
|
className="flex-1 overflow-y-auto pl-4 pr-4 md:pr-44"
|
||||||
>
|
>
|
||||||
<div className="flex flex-col justify-end min-h-full">
|
<div className="flex flex-col justify-end min-h-full">
|
||||||
{messages.map((message, index) => (
|
{messages?.map((message, index) => (
|
||||||
<Message
|
<Message
|
||||||
key={message.id}
|
key={message.id}
|
||||||
message={message}
|
message={message}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { memo, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
Tooltip,
|
Tooltip,
|
||||||
TooltipArrow,
|
TooltipArrow,
|
||||||
@@ -12,15 +13,13 @@ import {
|
|||||||
import { cn } from "@core/utils/cn.ts";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
import { Avatar } from "@components/UI/Avatar.tsx";
|
import { Avatar } from "@components/UI/Avatar.tsx";
|
||||||
import type { Protobuf } from "@meshtastic/core";
|
import type { Protobuf } from "@meshtastic/core";
|
||||||
import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react";
|
import { AlertCircle, CheckCircle2, CircleEllipsis, LucideIcon } from "lucide-react";
|
||||||
import type { LucideIcon } from "lucide-react";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
|
|
||||||
const MESSAGE_STATES = {
|
type MessageStateValue = {
|
||||||
ACK: "ack",
|
state: string;
|
||||||
WAITING: "waiting",
|
icon: LucideIcon;
|
||||||
FAILED: "failed",
|
displayText: string;
|
||||||
} as const;
|
}
|
||||||
|
|
||||||
type MessageState = MessageWithState["state"];
|
type MessageState = MessageWithState["state"];
|
||||||
|
|
||||||
@@ -40,31 +39,36 @@ interface StatusIconProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const STATUS_TEXT_MAP: Record<MessageState, string> = {
|
const MESSAGE_STATES: Record<string, MessageStateValue> = {
|
||||||
[MESSAGE_STATES.ACK]: "Message delivered",
|
ACK: { state: 'ack', icon: CheckCircle2, displayText: "Message delivered" },
|
||||||
[MESSAGE_STATES.WAITING]: "Waiting for delivery",
|
WAITING: { state: 'waiting', icon: CircleEllipsis, displayText: "Waiting for delivery" },
|
||||||
[MESSAGE_STATES.FAILED]: "Delivery failed",
|
FAILED: { state: 'failed', icon: AlertCircle, displayText: "Delivery failed" },
|
||||||
};
|
};
|
||||||
|
|
||||||
const STATUS_ICON_MAP: Record<MessageState, LucideIcon> = {
|
const getMessageState = (state: MessageState): MessageStateValue => {
|
||||||
[MESSAGE_STATES.ACK]: CheckCircle2,
|
switch (state) {
|
||||||
[MESSAGE_STATES.WAITING]: CircleEllipsis,
|
case MESSAGE_STATES.ACK.state:
|
||||||
[MESSAGE_STATES.FAILED]: AlertCircle,
|
return MESSAGE_STATES.ACK;
|
||||||
};
|
case MESSAGE_STATES.WAITING.state:
|
||||||
|
return MESSAGE_STATES.WAITING;
|
||||||
const getStatusText = (state: MessageState): string => STATUS_TEXT_MAP[state];
|
case MESSAGE_STATES.FAILED.state:
|
||||||
|
return MESSAGE_STATES.FAILED;
|
||||||
|
default:
|
||||||
|
return MESSAGE_STATES.FAILED;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const StatusTooltip = ({ state, children }: StatusTooltipProps) => (
|
const StatusTooltip = ({ state, children }: StatusTooltipProps) => (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||||
<TooltipContent
|
<TooltipContent
|
||||||
className="rounded-md bg-slate-800 px-3 py-1.5 text-sm text-white shadow-md animate-in fade-in-0 zoom-in-95"
|
className="rounded-md bg-slate-800 px-3 py-1.5 text-sm text-white dark:text-white shadow-md animate-in fade-in-0 zoom-in-95"
|
||||||
side="top"
|
side="top"
|
||||||
align="center"
|
align="center"
|
||||||
sideOffset={5}
|
sideOffset={5}
|
||||||
>
|
>
|
||||||
{getStatusText(state)}
|
{getMessageState(state).displayText ?? "An unknown error occurred"};
|
||||||
<TooltipArrow className="fill-slate-800" />
|
<TooltipArrow className="fill-slate-800" />
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
@@ -72,13 +76,17 @@ const StatusTooltip = ({ state, children }: StatusTooltipProps) => (
|
|||||||
);
|
);
|
||||||
|
|
||||||
const StatusIcon = ({ state, className, ...otherProps }: StatusIconProps) => {
|
const StatusIcon = ({ state, className, ...otherProps }: StatusIconProps) => {
|
||||||
const isFailed = state === MESSAGE_STATES.FAILED;
|
const msgState = getMessageState(state);
|
||||||
|
|
||||||
|
const isFailed = msgState.state === 'failed'
|
||||||
|
|
||||||
const iconClass = cn(
|
const iconClass = cn(
|
||||||
className,
|
className,
|
||||||
"text-slate-500 dark:text-slate-400 w-4 h-4 shrink-0",
|
"text-slate-500 dark:text-slate-400 size-5 shrink-0"
|
||||||
);
|
);
|
||||||
|
|
||||||
const Icon = STATUS_ICON_MAP[state];
|
const Icon = msgState.icon;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StatusTooltip state={state}>
|
<StatusTooltip state={state}>
|
||||||
<Icon
|
<Icon
|
||||||
@@ -90,23 +98,7 @@ const StatusIcon = ({ state, className, ...otherProps }: StatusIconProps) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const getMessageTextStyles = (state: MessageState) => {
|
const TimeDisplay = memo(({ date, className }: { date: Date; className?: string }) => (
|
||||||
const isAcknowledged = state === MESSAGE_STATES.ACK;
|
|
||||||
const isFailed = state === MESSAGE_STATES.FAILED;
|
|
||||||
|
|
||||||
return cn(
|
|
||||||
"break-words overflow-hidden",
|
|
||||||
isAcknowledged
|
|
||||||
? "text-slate-900 dark:text-white"
|
|
||||||
: "text-slate-900 dark:text-slate-400",
|
|
||||||
isFailed && "text-red-500 dark:text-red-500",
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const TimeDisplay = ({
|
|
||||||
date,
|
|
||||||
className,
|
|
||||||
}: { date: Date; className?: string }) => (
|
|
||||||
<div className={cn("flex items-center gap-2 shrink-0", className)}>
|
<div className={cn("flex items-center gap-2 shrink-0", className)}>
|
||||||
<span className="text-xs text-slate-500 dark:text-slate-400 font-mono">
|
<span className="text-xs text-slate-500 dark:text-slate-400 font-mono">
|
||||||
{date.toLocaleDateString()}
|
{date.toLocaleDateString()}
|
||||||
@@ -118,9 +110,9 @@ const TimeDisplay = ({
|
|||||||
})}
|
})}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
));
|
||||||
|
|
||||||
export const Message = ({ lastMsgSameUser, message, sender }: MessageProps) => {
|
export const Message = memo(({ lastMsgSameUser, message, sender }: MessageProps) => {
|
||||||
const { getDevices } = useDeviceStore();
|
const { getDevices } = useDeviceStore();
|
||||||
|
|
||||||
const isDeviceUser = useMemo(
|
const isDeviceUser = useMemo(
|
||||||
@@ -128,33 +120,47 @@ export const Message = ({ lastMsgSameUser, message, sender }: MessageProps) => {
|
|||||||
getDevices()
|
getDevices()
|
||||||
.map((device) => device.nodes.get(device.hardware.myNodeNum)?.num)
|
.map((device) => device.nodes.get(device.hardware.myNodeNum)?.num)
|
||||||
.includes(message.from),
|
.includes(message.from),
|
||||||
[getDevices, message.from],
|
[getDevices, message.from]
|
||||||
);
|
);
|
||||||
|
|
||||||
const messageUser = sender?.user;
|
const messageUser = sender?.user;
|
||||||
|
|
||||||
const messageTextClass = getMessageTextStyles(message.state);
|
const getMessageTextStyles = (state: MessageState) => {
|
||||||
|
const msgState = getMessageState(state);
|
||||||
|
const isAcknowledged = msgState.state === 'ack'
|
||||||
|
const isFailed = msgState.state === 'failed'
|
||||||
|
|
||||||
|
return cn(
|
||||||
|
"break-words overflow-hidden",
|
||||||
|
isAcknowledged
|
||||||
|
? "text-slate-900 dark:text-white"
|
||||||
|
: "text-slate-900 dark:text-slate-400",
|
||||||
|
isFailed && "text-red-500 dark:text-red-500",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const messageTextClass = useMemo(() => getMessageTextStyles(message.state), [message.state]);
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col w-full px-4 justify-start">
|
<div className="flex flex-col w-full px-4 justify-start">
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex flex-col flex-wrap items-start py-1",
|
"flex flex-col flex-wrap items-start py-1",
|
||||||
isDeviceUser && "items-end",
|
isDeviceUser && "items-end"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2 mb-2">
|
<div className="flex items-center gap-2 mb-2">
|
||||||
{!lastMsgSameUser
|
{!lastMsgSameUser && (
|
||||||
? (
|
<div className="flex place-items-center gap-2 mb-1">
|
||||||
<div className="flex place-items-center gap-2 mb-1">
|
<Avatar text={messageUser?.shortName ?? "UNK"} />
|
||||||
<Avatar text={messageUser?.shortName ?? "UNK"} />
|
<div className="flex flex-col">
|
||||||
<div className="flex flex-col">
|
<span className="font-medium text-slate-900 dark:text-white truncate">
|
||||||
<span className="font-medium text-slate-900 dark:text-white truncate">
|
{messageUser?.longName}
|
||||||
{messageUser?.longName}
|
</span>
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
</div>
|
||||||
: null}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<TimeDisplay date={message.rxTime} />
|
<TimeDisplay date={message.rxTime} />
|
||||||
<div className="flex place-items-center gap-2 pb-2">
|
<div className="flex place-items-center gap-2 pb-2">
|
||||||
@@ -166,4 +172,4 @@ export const Message = ({ lastMsgSameUser, message, sender }: MessageProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipArrow,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@components/UI/Tooltip.tsx";
|
||||||
|
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
||||||
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
import { Avatar } from "@components/UI/Avatar.tsx";
|
||||||
|
import { AlertCircle, CheckCircle2, CircleEllipsis } from "lucide-react";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
import { ReactNode, useMemo } from "react";
|
||||||
|
import { Message, MessageState } from "@core/services/types.ts";
|
||||||
|
|
||||||
|
interface MessageProps {
|
||||||
|
lastMsgSameUser: boolean;
|
||||||
|
message: Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MessageStatus {
|
||||||
|
state: MessageState;
|
||||||
|
displayText: string;
|
||||||
|
icon: LucideIcon;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MESSAGE_STATUS: Record<MessageState, MessageStatus> = {
|
||||||
|
ack: { state: "ack", displayText: "Message delivered", icon: CheckCircle2 },
|
||||||
|
waiting: { state: "waiting", displayText: "Waiting for delivery", icon: CircleEllipsis },
|
||||||
|
failed: { state: "failed", displayText: "Delivery failed", icon: AlertCircle },
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMessageStatus = (state: MessageState): MessageStatus =>
|
||||||
|
MESSAGE_STATUS[state] || { state: "failed", displayText: "Unknown error", icon: AlertCircle };
|
||||||
|
|
||||||
|
const StatusTooltip = ({ status, children }: { status: MessageStatus; children: ReactNode }) => (
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>{children}</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="rounded-md bg-slate-800 px-3 py-1.5 text-sm text-white shadow-md animate-in fade-in-0 zoom-in-95"
|
||||||
|
side="top"
|
||||||
|
align="center"
|
||||||
|
sideOffset={5}
|
||||||
|
>
|
||||||
|
{status.displayText}
|
||||||
|
<TooltipArrow className="fill-slate-800" />
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
const StatusIcon = ({ status, className, ...otherProps }: { status: MessageStatus; className?: string }) => {
|
||||||
|
const isFailed = status.state === "failed";
|
||||||
|
const iconClass = cn("text-slate-500 dark:text-slate-400 w-4 h-4 shrink-0", className);
|
||||||
|
const Icon = status.icon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StatusTooltip status={status}>
|
||||||
|
<Icon className={iconClass} {...otherProps} color={isFailed ? "red" : "currentColor"} />
|
||||||
|
</StatusTooltip>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMessageTextStyles = (status: MessageStatus) => {
|
||||||
|
const isAcknowledged = status.state === "ack";
|
||||||
|
const isFailed = status.state === "failed";
|
||||||
|
|
||||||
|
return cn(
|
||||||
|
"break-words overflow-hidden",
|
||||||
|
isAcknowledged ? "text-slate-900 dark:text-white" : "text-slate-900 dark:text-slate-400",
|
||||||
|
isFailed && "text-red-500 dark:text-red-500",
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const TimeDisplay = ({ date, className }: { date: Date; className?: string }) => (
|
||||||
|
<div className={cn("flex items-center gap-2 shrink-0", className)}>
|
||||||
|
<span className="text-xs text-slate-500 dark:text-slate-400 font-mono">{date.toLocaleDateString()}</span>
|
||||||
|
<span className="text-xs text-slate-500 dark:text-slate-400 font-mono">
|
||||||
|
{date.toLocaleTimeString(undefined, { hour: "2-digit", minute: "2-digit" })}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export const MessageItem = ({ lastMsgSameUser, message }: MessageProps) => {
|
||||||
|
const { getDevices } = useDeviceStore();
|
||||||
|
|
||||||
|
const isDeviceUser = useMemo(
|
||||||
|
() =>
|
||||||
|
getDevices()
|
||||||
|
.map((device) => device.nodes.get(device.hardware.myNodeNum)?.num)
|
||||||
|
.includes(message.from),
|
||||||
|
[getDevices, message.from],
|
||||||
|
);
|
||||||
|
|
||||||
|
const messageUser = message?.from
|
||||||
|
? getDevices().find((device) => device.nodes.has(message.from))?.nodes.get(message.from)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const messageStatus = getMessageStatus(message.state);
|
||||||
|
const messageTextClass = getMessageTextStyles(messageStatus);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col w-full px-4 justify-start">
|
||||||
|
<div className={cn("flex flex-col flex-wrap items-start py-1", messageTextClass, isDeviceUser && "items-end")}>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
{!lastMsgSameUser && (
|
||||||
|
<div className="flex place-items-center gap-2 mb-1">
|
||||||
|
<Avatar text={messageUser?.user?.shortName ?? "UNK"} />
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<span className="font-medium text-slate-900 dark:text-white truncate">
|
||||||
|
{messageUser?.user?.longName}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<TimeDisplay date={message.date} />
|
||||||
|
<div className="flex place-items-center gap-2 pb-2">
|
||||||
|
<div className={cn(isDeviceUser && "pl-11", messageTextClass)}>{message.message}</div>
|
||||||
|
<StatusIcon status={messageStatus} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { describe, it, expect, vi, beforeEach, Mock } from "vitest";
|
||||||
|
import { render, screen } from "@testing-library/react";
|
||||||
|
import { TraceRoute } from "@components/PageComponents/Messages/TraceRoute.tsx";
|
||||||
|
import { useDevice } from "@core/stores/deviceStore.ts";
|
||||||
|
|
||||||
|
vi.mock("@core/stores/deviceStore");
|
||||||
|
|
||||||
|
describe("TraceRoute", () => {
|
||||||
|
const mockNodes = new Map([
|
||||||
|
[
|
||||||
|
1,
|
||||||
|
{ num: 1, user: { longName: "Node A" } },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
2,
|
||||||
|
{ num: 2, user: { longName: "Node B" } },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
3,
|
||||||
|
{ num: 3, user: { longName: "Node C" } },
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
(useDevice as Mock).mockReturnValue({
|
||||||
|
nodes: mockNodes,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the route to destination with SNR values", () => {
|
||||||
|
render(
|
||||||
|
<TraceRoute
|
||||||
|
from={{ user: { longName: "Source Node" } } as any}
|
||||||
|
to={{ user: { longName: "Destination Node" } } as any}
|
||||||
|
route={[1, 2]}
|
||||||
|
snrTowards={[10, 20, 30]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Route to destination:")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Destination Node")).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText("Node A")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Node B")).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getAllByText(/↓/)).toHaveLength(3); // startNode + 2 hops
|
||||||
|
expect(screen.getByText("↓ 10dB")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("↓ 20dB")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("↓ 30dB")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Source Node")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders the route back when provided", () => {
|
||||||
|
render(
|
||||||
|
<TraceRoute
|
||||||
|
from={{ user: { longName: "Source Node" } } as any}
|
||||||
|
to={{ user: { longName: "Destination Node" } } as any}
|
||||||
|
route={[1]}
|
||||||
|
snrTowards={[15, 25]}
|
||||||
|
routeBack={[3]}
|
||||||
|
snrBack={[35, 45]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("Route back:")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("Node C")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("↓ 35dB")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("↓ 45dB")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders '??' for missing SNR values", () => {
|
||||||
|
render(
|
||||||
|
<TraceRoute
|
||||||
|
from={{ user: { longName: "Source" } } as any}
|
||||||
|
to={{ user: { longName: "Dest" } } as any}
|
||||||
|
route={[1]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getAllByText("↓ ??dB").length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("renders hop hex if node is not found", () => {
|
||||||
|
render(
|
||||||
|
<TraceRoute
|
||||||
|
from={{ user: { longName: "Source" } } as any}
|
||||||
|
to={{ user: { longName: "Dest" } } as any}
|
||||||
|
route={[99]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText(/^!63$/)).toBeInTheDocument(); // 99 in hex
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -11,6 +11,33 @@ export interface TraceRouteProps {
|
|||||||
snrBack?: Array<number>;
|
snrBack?: Array<number>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface RoutePathProps {
|
||||||
|
title: string;
|
||||||
|
startNode?: Protobuf.Mesh.NodeInfo;
|
||||||
|
endNode?: Protobuf.Mesh.NodeInfo;
|
||||||
|
path: number[];
|
||||||
|
snr?: number[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const RoutePath = ({ title, startNode, endNode, path, snr }: RoutePathProps) => {
|
||||||
|
const { nodes } = useDevice();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className="ml-4 border-l-2 border-l-background-primary pl-2 text-slate-900 dark:text-slate-900">
|
||||||
|
<p className="font-semibold">{title}</p>
|
||||||
|
<p>{startNode?.user?.longName}</p>
|
||||||
|
<p>↓ {snr?.[0] ?? "??"}dB</p>
|
||||||
|
{path.map((hop, i) => (
|
||||||
|
<span key={nodes.get(hop)?.num ?? hop}>
|
||||||
|
<p>{nodes.get(hop)?.user?.longName ?? `!${numberToHexUnpadded(hop)}`}</p>
|
||||||
|
<p>↓ {snr?.[i + 1] ?? "??"}dB</p>
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
<p>{endNode?.user?.longName}</p>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const TraceRoute = ({
|
export const TraceRoute = ({
|
||||||
from,
|
from,
|
||||||
to,
|
to,
|
||||||
@@ -19,43 +46,24 @@ export const TraceRoute = ({
|
|||||||
snrTowards,
|
snrTowards,
|
||||||
snrBack,
|
snrBack,
|
||||||
}: TraceRouteProps) => {
|
}: TraceRouteProps) => {
|
||||||
const { nodes } = useDevice();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="ml-5 flex">
|
<div className="ml-5 flex">
|
||||||
<span className="ml-4 border-l-2 border-l-background-primary pl-2 text-text-primary">
|
<RoutePath
|
||||||
<p className="font-semibold">Route to destination:</p>
|
title="Route to destination:"
|
||||||
<p>{to?.user?.longName}</p>
|
startNode={to}
|
||||||
<p>↓ {snrTowards?.[0] ? snrTowards[0] : "??"}dB</p>
|
endNode={from}
|
||||||
{route.map((hop, i) => (
|
path={route}
|
||||||
<span key={nodes.get(hop)?.num}>
|
snr={snrTowards}
|
||||||
<p>
|
/>
|
||||||
{nodes.get(hop)?.user?.longName ?? `!${numberToHexUnpadded(hop)}`}
|
{routeBack && (
|
||||||
</p>
|
<RoutePath
|
||||||
<p>↓ {snrTowards?.[i + 1] ? snrTowards[i + 1] : "??"}dB</p>
|
title="Route back:"
|
||||||
</span>
|
startNode={from}
|
||||||
))}
|
endNode={to}
|
||||||
{from?.user?.longName}
|
path={routeBack}
|
||||||
</span>
|
snr={snrBack}
|
||||||
{routeBack
|
/>
|
||||||
? (
|
)}
|
||||||
<span className="ml-4 border-l-2 border-l-background-primary pl-2 text-text-primary">
|
|
||||||
<p className="font-semibold">Route back:</p>
|
|
||||||
<p>{from?.user?.longName}</p>
|
|
||||||
<p>↓ {snrBack?.[0] ? snrBack[0] : "??"}dB</p>
|
|
||||||
{routeBack.map((hop, i) => (
|
|
||||||
<span key={nodes.get(hop)?.num}>
|
|
||||||
<p>
|
|
||||||
{nodes.get(hop)?.user?.longName ??
|
|
||||||
`!${numberToHexUnpadded(hop)}`}
|
|
||||||
</p>
|
|
||||||
<p>↓ {snrBack?.[i + 1] ? snrBack[i + 1] : "??"}dB</p>
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
{to?.user?.longName}
|
|
||||||
</span>
|
|
||||||
)
|
|
||||||
: null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ export const Sidebar = ({ children }: SidebarProps) => {
|
|||||||
page: "channels",
|
page: "channels",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Nodes",
|
name: `Nodes (${nodes.size})`,
|
||||||
icon: UsersIcon,
|
icon: UsersIcon,
|
||||||
page: "nodes",
|
page: "nodes",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -24,18 +24,25 @@ export default function ThemeSwitcher({
|
|||||||
setPreference(nextPreference);
|
setPreference(nextPreference);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const [firstCharOfPreference = "", ...restOfPreference] = preference;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className={cn(
|
className={cn(
|
||||||
"transition-all duration-300 scale-100 cursor-pointer m-6 p-2",
|
"transition-all duration-300 scale-100 cursor-pointer m-6 p-2 focus:*:data-label:opacity-100",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
onClick={toggleTheme}
|
onClick={toggleTheme}
|
||||||
aria-label={preference === "system"
|
aria-description={"Change current theme"}
|
||||||
? `System theme (currently ${theme}). Click to change theme.`
|
|
||||||
: `Current theme: ${theme}. Click to change theme.`}
|
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
|
data-label
|
||||||
|
className="transition-all block absolute w-full mb-auto mt-auto ml-0 mr-0 text-xs left-0 -top-5 opacity-0 rounded-lg"
|
||||||
|
>
|
||||||
|
{firstCharOfPreference.toLocaleUpperCase() +
|
||||||
|
(restOfPreference ?? []).join("")}
|
||||||
|
</span>
|
||||||
{themeIcons[preference]}
|
{themeIcons[preference]}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { cn } from "../../core/utils/cn.ts";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
import { LockKeyholeOpenIcon } from 'lucide-react';
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
type RGBColor = {
|
type RGBColor = {
|
||||||
@@ -12,6 +13,7 @@ interface AvatarProps {
|
|||||||
text: string;
|
text: string;
|
||||||
size?: "sm" | "lg";
|
size?: "sm" | "lg";
|
||||||
className?: string;
|
className?: string;
|
||||||
|
showError?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// biome-ignore lint/complexity/noStaticOnlyClass: stop being annoying Biome
|
// biome-ignore lint/complexity/noStaticOnlyClass: stop being annoying Biome
|
||||||
@@ -43,6 +45,7 @@ class ColorUtils {
|
|||||||
export const Avatar: React.FC<AvatarProps> = ({
|
export const Avatar: React.FC<AvatarProps> = ({
|
||||||
text,
|
text,
|
||||||
size = "sm",
|
size = "sm",
|
||||||
|
showError = false,
|
||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const sizes = {
|
const sizes = {
|
||||||
@@ -73,12 +76,11 @@ export const Avatar: React.FC<AvatarProps> = ({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
`
|
`flex
|
||||||
|
relative
|
||||||
rounded-full
|
rounded-full
|
||||||
flex
|
|
||||||
items-center
|
items-center
|
||||||
justify-center
|
justify-center
|
||||||
size-11
|
|
||||||
font-semibold`,
|
font-semibold`,
|
||||||
sizes[size],
|
sizes[size],
|
||||||
className,
|
className,
|
||||||
@@ -88,6 +90,7 @@ export const Avatar: React.FC<AvatarProps> = ({
|
|||||||
color: textColor,
|
color: textColor,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
{showError ? <LockKeyholeOpenIcon className="size-4 absolute bottom-0 right-0 z-10 text-red-500 stroke-3" /> : null}
|
||||||
<p className="p-1">{initials}</p>
|
<p className="p-1">{initials}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const buttonVariants = cva(
|
|||||||
success:
|
success:
|
||||||
"bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600",
|
"bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600",
|
||||||
outline:
|
outline:
|
||||||
"bg-transparent border border-slate-200 hover:bg-slate-100 dark:border-slate-400 dark:text-slate-500",
|
"bg-transparent border border-slate-400 hover:bg-slate-100 dark:border-slate-400 dark:text-slate-500",
|
||||||
subtle:
|
subtle:
|
||||||
"bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-500 dark:text-white dark:hover:bg-slate-400",
|
"bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-500 dark:text-white dark:hover:bg-slate-400",
|
||||||
ghost:
|
ghost:
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ export const Table = ({ headings, rows }: TableProps) => {
|
|||||||
<th
|
<th
|
||||||
key={heading.title}
|
key={heading.title}
|
||||||
scope="col"
|
scope="col"
|
||||||
className={`py-2 pr-3 pl-6 text-left ${
|
className={`py-2 pr-3 text-left ${
|
||||||
heading.sortable
|
heading.sortable
|
||||||
? "cursor-pointer hover:brightness-hover active:brightness-press"
|
? "cursor-pointer hover:brightness-hover active:brightness-press"
|
||||||
: ""
|
: ""
|
||||||
@@ -114,8 +114,16 @@ export const Table = ({ headings, rows }: TableProps) => {
|
|||||||
<tbody>
|
<tbody>
|
||||||
{sortedRows.map((row, index) => (
|
{sortedRows.map((row, index) => (
|
||||||
// biome-ignore lint/suspicious/noArrayIndexKey: TODO: Once this table is sortable, this should get fixed.
|
// biome-ignore lint/suspicious/noArrayIndexKey: TODO: Once this table is sortable, this should get fixed.
|
||||||
<tr key={index}>
|
<tr key={index} className={`${index % 2 ? 'bg-white dark:bg-white/2' : 'bg-slate-50/50 dark:bg-slate-50/5'} border-b-1 border-slate-200 dark:border-slate-900`}>
|
||||||
{row.map((item, index) => (
|
{row.map((item, index) => (
|
||||||
|
index === 0 ?
|
||||||
|
<th
|
||||||
|
key={item.key ?? index}
|
||||||
|
className="whitespace-nowrap py-2 text-sm text-text-secondary first:pl-2"
|
||||||
|
scope="row"
|
||||||
|
>
|
||||||
|
{item}
|
||||||
|
</th> :
|
||||||
<td
|
<td
|
||||||
key={item.key ?? index}
|
key={item.key ?? index}
|
||||||
className="whitespace-nowrap py-2 text-sm text-text-secondary first:pl-2"
|
className="whitespace-nowrap py-2 text-sm text-text-secondary first:pl-2"
|
||||||
|
|||||||
@@ -27,12 +27,18 @@ export type DialogVariant =
|
|||||||
| "nodeRemoval"
|
| "nodeRemoval"
|
||||||
| "pkiBackup"
|
| "pkiBackup"
|
||||||
| "nodeDetails"
|
| "nodeDetails"
|
||||||
| "unsafeRoles";
|
| "unsafeRoles"
|
||||||
|
| "refreshKeys";
|
||||||
|
|
||||||
type QueueStatus = {
|
type QueueStatus = {
|
||||||
res: number, free: number, maxlen: number
|
res: number, free: number, maxlen: number
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NodeError = {
|
||||||
|
node: number;
|
||||||
|
error: string;
|
||||||
|
}
|
||||||
|
|
||||||
export interface Device {
|
export interface Device {
|
||||||
id: number;
|
id: number;
|
||||||
status: Types.DeviceStatusEnum;
|
status: Types.DeviceStatusEnum;
|
||||||
@@ -52,6 +58,7 @@ export interface Device {
|
|||||||
number,
|
number,
|
||||||
Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>[]
|
Types.PacketMetadata<Protobuf.Mesh.RouteDiscovery>[]
|
||||||
>;
|
>;
|
||||||
|
nodeErrors: Map<number, NodeError>;
|
||||||
connection?: MeshDevice;
|
connection?: MeshDevice;
|
||||||
activePage: Page;
|
activePage: Page;
|
||||||
activeNode: number;
|
activeNode: number;
|
||||||
@@ -71,6 +78,7 @@ export interface Device {
|
|||||||
pkiBackup: boolean;
|
pkiBackup: boolean;
|
||||||
nodeDetails: boolean;
|
nodeDetails: boolean;
|
||||||
unsafeRoles: boolean;
|
unsafeRoles: boolean;
|
||||||
|
refreshKeys: boolean;
|
||||||
};
|
};
|
||||||
unreadCounts: Map<number, number>;
|
unreadCounts: Map<number, number>;
|
||||||
|
|
||||||
@@ -111,6 +119,10 @@ export interface Device {
|
|||||||
setMessageDraft: (message: string) => void;
|
setMessageDraft: (message: string) => void;
|
||||||
setUnread: (id: number, count: number) => void;
|
setUnread: (id: number, count: number) => void;
|
||||||
setQueueStatus: (status: QueueStatus) => void;
|
setQueueStatus: (status: QueueStatus) => void;
|
||||||
|
setNodeError: (nodeNum: number, error: string) => void;
|
||||||
|
clearNodeError: (nodeNum: number) => void;
|
||||||
|
getNodeError: (nodeNum: number) => NodeError | undefined;
|
||||||
|
hasNodeError: (nodeNum: number) => boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceState {
|
export interface DeviceState {
|
||||||
@@ -164,10 +176,12 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
|||||||
pkiBackup: false,
|
pkiBackup: false,
|
||||||
nodeDetails: false,
|
nodeDetails: false,
|
||||||
unsafeRoles: false,
|
unsafeRoles: false,
|
||||||
|
refreshKeys: false,
|
||||||
},
|
},
|
||||||
pendingSettingsChanges: false,
|
pendingSettingsChanges: false,
|
||||||
messageDraft: "",
|
messageDraft: "",
|
||||||
unreadCounts: new Map(),
|
unreadCounts: new Map(),
|
||||||
|
nodeErrors: new Map(),
|
||||||
|
|
||||||
setStatus: (status: Types.DeviceStatusEnum) => {
|
setStatus: (status: Types.DeviceStatusEnum) => {
|
||||||
set(
|
set(
|
||||||
@@ -534,7 +548,6 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
|||||||
addTraceRoute: (traceroute) => {
|
addTraceRoute: (traceroute) => {
|
||||||
set(
|
set(
|
||||||
produce<DeviceState>((draft) => {
|
produce<DeviceState>((draft) => {
|
||||||
console.log("addTraceRoute called");
|
|
||||||
const device = draft.devices.get(id);
|
const device = draft.devices.get(id);
|
||||||
if (!device) {
|
if (!device) {
|
||||||
return;
|
return;
|
||||||
@@ -571,10 +584,8 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
|||||||
) => {
|
) => {
|
||||||
set(
|
set(
|
||||||
produce<DeviceState>((draft) => {
|
produce<DeviceState>((draft) => {
|
||||||
console.log("setMessageState called");
|
|
||||||
const device = draft.devices.get(id);
|
const device = draft.devices.get(id);
|
||||||
if (!device) {
|
if (!device) {
|
||||||
console.log("no device found for id");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const messageGroup = device.messages[type];
|
const messageGroup = device.messages[type];
|
||||||
@@ -585,7 +596,6 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
|||||||
const messages = messageGroup.get(messageIndex);
|
const messages = messageGroup.get(messageIndex);
|
||||||
|
|
||||||
if (!messages) {
|
if (!messages) {
|
||||||
console.log("no messages found for id");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -680,7 +690,42 @@ export const useDeviceStore = createStore<DeviceState>((set, get) => ({
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
|
setNodeError: (nodeNum, error) => {
|
||||||
|
set(
|
||||||
|
produce<DeviceState>((draft) => {
|
||||||
|
const device = draft.devices.get(id);
|
||||||
|
if (device) {
|
||||||
|
device.nodeErrors.set(nodeNum, { node: nodeNum, error });
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
clearNodeError: (nodeNum: number) => {
|
||||||
|
set(
|
||||||
|
produce<DeviceState>((draft) => {
|
||||||
|
const device = draft.devices.get(id);
|
||||||
|
if (device) {
|
||||||
|
device.nodeErrors.delete(nodeNum);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
getNodeError: (nodeNum: number) => {
|
||||||
|
const device = get().devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
throw new Error("Device not found");
|
||||||
|
}
|
||||||
|
return device.nodeErrors.get(nodeNum);
|
||||||
|
},
|
||||||
|
hasNodeError: (nodeNum: number) => {
|
||||||
|
const device = get().devices.get(id);
|
||||||
|
if (!device) {
|
||||||
|
throw new Error("Device not found");
|
||||||
|
}
|
||||||
|
return device.nodeErrors.has(nodeNum);
|
||||||
|
},
|
||||||
|
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|||||||
+24
-10
@@ -23,15 +23,15 @@ export const subscribeAll = (
|
|||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
console.log(`Routing Error: ${routingPacket.data.variant.value}`);
|
console.info(`Routing Error: ${routingPacket.data.variant.value}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "routeReply": {
|
case "routeReply": {
|
||||||
console.log(`Route Reply: ${routingPacket.data.variant.value}`);
|
console.info(`Route Reply: ${routingPacket.data.variant.value}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "routeRequest": {
|
case "routeRequest": {
|
||||||
console.log(`Route Request: ${routingPacket.data.variant.value}`);
|
console.info(`Route Request: ${routingPacket.data.variant.value}`);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -71,8 +71,6 @@ export const subscribeAll = (
|
|||||||
});
|
});
|
||||||
|
|
||||||
connection.events.onChannelPacket.subscribe((channel) => {
|
connection.events.onChannelPacket.subscribe((channel) => {
|
||||||
console.log('channel', channel);
|
|
||||||
|
|
||||||
device.addChannel(channel);
|
device.addChannel(channel);
|
||||||
});
|
});
|
||||||
connection.events.onConfigPacket.subscribe((config) => {
|
connection.events.onConfigPacket.subscribe((config) => {
|
||||||
@@ -82,10 +80,8 @@ export const subscribeAll = (
|
|||||||
device.setModuleConfig(moduleConfig);
|
device.setModuleConfig(moduleConfig);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
connection.events.onMessagePacket.subscribe((messagePacket) => {
|
connection.events.onMessagePacket.subscribe((messagePacket) => {
|
||||||
|
|
||||||
console.log('messagePacket', messagePacket);
|
|
||||||
|
|
||||||
device.addMessage({
|
device.addMessage({
|
||||||
...messagePacket,
|
...messagePacket,
|
||||||
state: messagePacket.from !== myNodeNum ? "ack" : "waiting",
|
state: messagePacket.from !== myNodeNum ? "ack" : "waiting",
|
||||||
@@ -120,8 +116,26 @@ export const subscribeAll = (
|
|||||||
|
|
||||||
connection.events.onQueueStatus.subscribe((queueStatus) => {
|
connection.events.onQueueStatus.subscribe((queueStatus) => {
|
||||||
device.setQueueStatus(queueStatus);
|
device.setQueueStatus(queueStatus);
|
||||||
if (queueStatus.free < 10) {
|
});
|
||||||
// start queueing messages
|
|
||||||
|
connection.events.onRoutingPacket.subscribe((routingPacket) => {
|
||||||
|
if (routingPacket.data.variant.case === "errorReason") {
|
||||||
|
switch (routingPacket.data.variant.value) {
|
||||||
|
case Protobuf.Mesh.Routing_Error.NO_CHANNEL:
|
||||||
|
console.error(`Routing Error: ${routingPacket.data.variant.value}`);
|
||||||
|
device.setNodeError(routingPacket.from, Protobuf.Mesh.Routing_Error[routingPacket?.data?.variant?.value]);
|
||||||
|
device.setDialogOpen("refreshKeys", true);
|
||||||
|
break;
|
||||||
|
case Protobuf.Mesh.Routing_Error.PKI_UNKNOWN_PUBKEY:
|
||||||
|
console.error(`Routing Error: ${routingPacket.data.variant.value}`);
|
||||||
|
device.setNodeError(routingPacket.from, Protobuf.Mesh.Routing_Error[routingPacket?.data?.variant?.value]);
|
||||||
|
device.setDialogOpen("refreshKeys", true);
|
||||||
|
break;
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { scan } from "react-scan";
|
|
||||||
import "@app/index.css";
|
import "@app/index.css";
|
||||||
import { enableMapSet } from "immer";
|
import { enableMapSet } from "immer";
|
||||||
import "maplibre-gl/dist/maplibre-gl.css";
|
import "maplibre-gl/dist/maplibre-gl.css";
|
||||||
@@ -7,13 +6,6 @@ import { createRoot } from "react-dom/client";
|
|||||||
|
|
||||||
import { App } from "@app/App.tsx";
|
import { App } from "@app/App.tsx";
|
||||||
|
|
||||||
// run react scan tool in development mode only
|
|
||||||
// react scan must be the first import and the first line in this file in order to work properly
|
|
||||||
import.meta.env.VITE_DEBUG_SCAN &&
|
|
||||||
scan({
|
|
||||||
enabled: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const container = document.getElementById("root") as HTMLElement;
|
const container = document.getElementById("root") as HTMLElement;
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
|
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ import { getChannelName } from "@pages/Channels.tsx";
|
|||||||
import { HashIcon, LockIcon, LockOpenIcon } from "lucide-react";
|
import { HashIcon, LockIcon, LockOpenIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { MessageInput } from "@components/PageComponents/Messages/MessageInput.tsx";
|
import { MessageInput } from "@components/PageComponents/Messages/MessageInput.tsx";
|
||||||
|
import { cn } from "@core/utils/cn.ts";
|
||||||
|
|
||||||
export const MessagesPage = () => {
|
export const MessagesPage = () => {
|
||||||
const { channels, nodes, hardware, messages, unreadCounts, setUnread } = useDevice();
|
const { channels, nodes, hardware, messages, hasNodeError, unreadCounts, setUnread } = useDevice();
|
||||||
const { activeChat, chatType, setActiveChat, setChatType } = useAppStore();
|
const { activeChat, chatType, setActiveChat, setChatType } = useAppStore();
|
||||||
const [searchTerm, setSearchTerm] = useState<string>("");
|
const [searchTerm, setSearchTerm] = useState<string>("");
|
||||||
const filteredNodes = Array.from(nodes.values()).filter((node) => {
|
const filteredNodes = Array.from(nodes.values()).filter((node) => {
|
||||||
@@ -91,6 +92,8 @@ export const MessagesPage = () => {
|
|||||||
element={
|
element={
|
||||||
<Avatar
|
<Avatar
|
||||||
text={node.user?.shortName ?? node.num.toString()}
|
text={node.user?.shortName ?? node.num.toString()}
|
||||||
|
className={cn(hasNodeError(node.num) && "text-red-500")}
|
||||||
|
showError={hasNodeError(node.num)}
|
||||||
size="sm"
|
size="sm"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -154,7 +157,6 @@ export const MessagesPage = () => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Single message input for both chat types */}
|
|
||||||
<div className="shrink-0 p-4 w-full dark:bg-slate-900">
|
<div className="shrink-0 p-4 w-full dark:bg-slate-900">
|
||||||
<MessageInput
|
<MessageInput
|
||||||
to={messageDestination}
|
to={messageDestination}
|
||||||
|
|||||||
+55
-56
@@ -19,10 +19,22 @@ export interface DeleteNoteDialogProps {
|
|||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shortNameFromNode(
|
||||||
|
node: ReturnType<useDevice>["nodes"][number],
|
||||||
|
): string {
|
||||||
|
const shortNameOfNode = node.user?.shortName ??
|
||||||
|
(node.user?.macaddr
|
||||||
|
? `${
|
||||||
|
base16
|
||||||
|
.stringify(node.user?.macaddr.subarray(4, 6) ?? [])
|
||||||
|
.toLowerCase()
|
||||||
|
}`
|
||||||
|
: `${numberToHexUnpadded(node.num).slice(-4)}`);
|
||||||
|
return String(shortNameOfNode);
|
||||||
|
}
|
||||||
|
|
||||||
const NodesPage = (): JSX.Element => {
|
const NodesPage = (): JSX.Element => {
|
||||||
const { nodes, hardware, connection } = useDevice();
|
const { nodes, hardware, connection } = useDevice();
|
||||||
console.log(connection);
|
|
||||||
|
|
||||||
const [selectedNode, setSelectedNode] = useState<
|
const [selectedNode, setSelectedNode] = useState<
|
||||||
Protobuf.Mesh.NodeInfo | undefined
|
Protobuf.Mesh.NodeInfo | undefined
|
||||||
>(undefined);
|
>(undefined);
|
||||||
@@ -63,7 +75,6 @@ const NodesPage = (): JSX.Element => {
|
|||||||
};
|
};
|
||||||
}, [connection]);
|
}, [connection]);
|
||||||
|
|
||||||
|
|
||||||
const handleLocation = useCallback(
|
const handleLocation = useCallback(
|
||||||
(location: Types.PacketMetadata<Protobuf.Mesh.Position>) => {
|
(location: Types.PacketMetadata<Protobuf.Mesh.Position>) => {
|
||||||
if (location.to.valueOf() !== hardware.myNodeNum) return;
|
if (location.to.valueOf() !== hardware.myNodeNum) return;
|
||||||
@@ -89,46 +100,62 @@ const NodesPage = (): JSX.Element => {
|
|||||||
<Table
|
<Table
|
||||||
headings={[
|
headings={[
|
||||||
{ title: "", type: "blank", sortable: false },
|
{ title: "", type: "blank", sortable: false },
|
||||||
{ title: "Short Name", type: "normal", sortable: true },
|
|
||||||
{ title: "Long Name", type: "normal", sortable: true },
|
{ title: "Long Name", type: "normal", sortable: true },
|
||||||
|
{ title: "Connection", type: "normal", sortable: true },
|
||||||
|
{ title: "Last Heard", type: "normal", sortable: true },
|
||||||
|
{ title: "Encryption", type: "normal", sortable: false },
|
||||||
|
{ title: "SNR", type: "normal", sortable: true },
|
||||||
{ title: "Model", type: "normal", sortable: true },
|
{ title: "Model", type: "normal", sortable: true },
|
||||||
{ title: "MAC Address", type: "normal", sortable: true },
|
{ title: "MAC Address", type: "normal", sortable: true },
|
||||||
{ title: "Last Heard", type: "normal", sortable: true },
|
|
||||||
{ title: "SNR", type: "normal", sortable: true },
|
|
||||||
{ title: "Encryption", type: "normal", sortable: false },
|
|
||||||
{ title: "Connection", type: "normal", sortable: true },
|
|
||||||
]}
|
]}
|
||||||
rows={filteredNodes.map((node) => [
|
rows={filteredNodes.map((node) => [
|
||||||
<div key={node.num}>
|
<div key={node.num}>
|
||||||
<Avatar text={node.user?.shortName.toString() ?? "UNK"} />
|
<Avatar text={shortNameFromNode(node)} />
|
||||||
</div>,
|
</div>,
|
||||||
|
|
||||||
<h1
|
|
||||||
key="shortName"
|
|
||||||
onMouseDown={() => setSelectedNode(node)}
|
|
||||||
className="cursor-pointer"
|
|
||||||
>
|
|
||||||
{node.user?.shortName ??
|
|
||||||
(node.user?.macaddr
|
|
||||||
? `${base16
|
|
||||||
.stringify(node.user?.macaddr.subarray(4, 6) ?? [])
|
|
||||||
.toLowerCase()}`
|
|
||||||
: `${numberToHexUnpadded(node.num).slice(-4)}`)}
|
|
||||||
</h1>,
|
|
||||||
|
|
||||||
<h1
|
<h1
|
||||||
key="longName"
|
key="longName"
|
||||||
onMouseDown={() => setSelectedNode(node)}
|
onMouseDown={() => setSelectedNode(node)}
|
||||||
className="cursor-pointer"
|
onKeyUp={(evt) => {
|
||||||
|
evt.key === "Enter" && setSelectedNode(node);
|
||||||
|
}}
|
||||||
|
className="cursor-pointer underline"
|
||||||
|
tabIndex={0}
|
||||||
|
role="button"
|
||||||
>
|
>
|
||||||
{node.user?.longName ??
|
{node.user?.longName ??
|
||||||
(node.user?.macaddr
|
(node.user?.macaddr
|
||||||
? `Meshtastic ${base16
|
? `Meshtastic ${
|
||||||
.stringify(node.user?.macaddr.subarray(4, 6) ?? [])
|
base16
|
||||||
.toLowerCase()}`
|
.stringify(node.user?.macaddr.subarray(4, 6) ?? [])
|
||||||
|
.toLowerCase()
|
||||||
|
}`
|
||||||
: `!${numberToHexUnpadded(node.num)}`)}
|
: `!${numberToHexUnpadded(node.num)}`)}
|
||||||
</h1>,
|
</h1>,
|
||||||
|
<Mono key="hops">
|
||||||
|
{node.lastHeard !== 0
|
||||||
|
? node.viaMqtt === false && node.hopsAway === 0
|
||||||
|
? "Direct"
|
||||||
|
: `${node.hopsAway?.toString()} ${
|
||||||
|
node.hopsAway > 1 ? "hops" : "hop"
|
||||||
|
} away`
|
||||||
|
: "-"}
|
||||||
|
{node.viaMqtt === true ? ", via MQTT" : ""}
|
||||||
|
</Mono>,
|
||||||
|
<Mono key="lastHeard">
|
||||||
|
{node.lastHeard === 0
|
||||||
|
? <p>Never</p>
|
||||||
|
: <TimeAgo timestamp={node.lastHeard * 1000} />}
|
||||||
|
</Mono>,
|
||||||
|
<Mono key="pki">
|
||||||
|
{node.user?.publicKey && node.user?.publicKey.length > 0
|
||||||
|
? <LockIcon className="text-green-600 mx-auto" />
|
||||||
|
: <LockOpenIcon className="text-yellow-300 mx-auto" />}
|
||||||
|
</Mono>,
|
||||||
|
<Mono key="snr">
|
||||||
|
{node.snr}db/
|
||||||
|
{Math.min(Math.max((node.snr + 10) * 5, 0), 100)}%/
|
||||||
|
{(node.snr + 10) * 5}raw
|
||||||
|
</Mono>,
|
||||||
<Mono key="model">
|
<Mono key="model">
|
||||||
{Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]}
|
{Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]}
|
||||||
</Mono>,
|
</Mono>,
|
||||||
@@ -138,34 +165,6 @@ const NodesPage = (): JSX.Element => {
|
|||||||
.match(/.{1,2}/g)
|
.match(/.{1,2}/g)
|
||||||
?.join(":") ?? "UNK"}
|
?.join(":") ?? "UNK"}
|
||||||
</Mono>,
|
</Mono>,
|
||||||
<Mono className="px-4" key="lastHeard">
|
|
||||||
{node.lastHeard === 0 ? (
|
|
||||||
<p className="px-4">Never</p>
|
|
||||||
) : (
|
|
||||||
<TimeAgo timestamp={node.lastHeard * 1000} />
|
|
||||||
)}
|
|
||||||
</Mono>,
|
|
||||||
<Mono key="snr">
|
|
||||||
{node.snr}db/
|
|
||||||
{Math.min(Math.max((node.snr + 10) * 5, 0), 100)}%/
|
|
||||||
{(node.snr + 10) * 5}raw
|
|
||||||
</Mono>,
|
|
||||||
<Mono key="pki">
|
|
||||||
{node.user?.publicKey && node.user?.publicKey.length > 0 ? (
|
|
||||||
<LockIcon className="text-green-600 mx-auto" />
|
|
||||||
) : (
|
|
||||||
<LockOpenIcon className="text-yellow-300 mx-auto" />
|
|
||||||
)}
|
|
||||||
</Mono>,
|
|
||||||
<Mono key="hops">
|
|
||||||
{node.lastHeard !== 0
|
|
||||||
? node.viaMqtt === false && node.hopsAway === 0
|
|
||||||
? "Direct"
|
|
||||||
: `${node.hopsAway?.toString()} ${node.hopsAway > 1 ? "hops" : "hop"
|
|
||||||
} away`
|
|
||||||
: "-"}
|
|
||||||
{node.viaMqtt === true ? ", via MQTT" : ""}
|
|
||||||
</Mono>,
|
|
||||||
])}
|
])}
|
||||||
/>
|
/>
|
||||||
<NodeOptionsDialog
|
<NodeOptionsDialog
|
||||||
|
|||||||
+3
-6
@@ -44,11 +44,8 @@ export default defineConfig({
|
|||||||
server: {
|
server: {
|
||||||
port: 3000,
|
port: 3000,
|
||||||
headers: {
|
headers: {
|
||||||
'Cross-Origin-Opener-Policy': 'same-origin',
|
"Cross-Origin-Opener-Policy": "same-origin",
|
||||||
'Cross-Origin-Embedder-Policy': 'require-corp',
|
"Cross-Origin-Embedder-Policy": "require-corp",
|
||||||
}
|
},
|
||||||
},
|
|
||||||
optimizeDeps: {
|
|
||||||
exclude: ['react-scan']
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user