diff --git a/.gitignore b/.gitignore index 4cc7172b..ec3dfc3a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,6 @@ dist node_modules stats.html .vercel -.vite/deps +.vite dev-dist __screenshots__* \ No newline at end of file diff --git a/README.md b/README.md index 10e4678d..7bd44dc5 100644 --- a/README.md +++ b/README.md @@ -139,46 +139,28 @@ reasons: - **Web Standard APIs**: Uses browser-compatible APIs, making code more portable between server and client environments. -### Debugging - -#### Debugging with React Scan - -Meshtastic Web Client has included the library -[React Scan](https://github.com/aidenybai/react-scan) to help you identify and -resolve render performance issues during development. - -React's comparison-by-reference approach to props makes it easy to inadvertently -cause unnecessary re-renders, especially with: - -- Inline function callbacks (`onClick={() => handleClick()}`) -- Object literals (`style={{ color: "purple" }}`) -- Array literals (`items={[1, 2, 3]}`) - -These are recreated on every render, causing child components to re-render even -when nothing has actually changed. - -Unlike React DevTools, React Scan specifically focuses on performance -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. +### Contributing + +We welcome contributions! Here’s how the deployment flow works for pull +requests: + +- **Preview Deployments:**\ + Every pull request automatically generates a preview deployment on Vercel. + This allows you and reviewers to easily preview changes before merging. + +- **Staging Environment (`client-test`):**\ + 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. + +- **Production Releases:**\ + At regular intervals, stable and fully tested releases are promoted to our + production site: [client.meshtastic.org](https://client.meshtastic.org/).\ + This is the primary interface used by the public to connect with their + Meshtastic nodes. + +Please review our +[Contribution Guidelines](https://github.com/meshtastic/web/blob/master/CONTRIBUTING.md) +before submitting a pull request. We appreciate your help in making the project +better! diff --git a/deno.lock b/deno.lock index b7f18099..46190061 100644 --- a/deno.lock +++ b/deno.lock @@ -56,7 +56,6 @@ "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-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:rfc4648@^1.5.4": "1.5.4", "npm:simple-git-hooks@^2.11.1": "2.11.1", @@ -899,168 +898,78 @@ "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": { "integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==" }, - "@esbuild/android-arm64@0.24.2": { - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==" - }, "@esbuild/android-arm64@0.25.0": { "integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==" }, - "@esbuild/android-arm@0.24.2": { - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==" - }, "@esbuild/android-arm@0.25.0": { "integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==" }, - "@esbuild/android-x64@0.24.2": { - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==" - }, "@esbuild/android-x64@0.25.0": { "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": { "integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==" }, - "@esbuild/darwin-x64@0.24.2": { - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==" - }, "@esbuild/darwin-x64@0.25.0": { "integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==" }, - "@esbuild/freebsd-arm64@0.24.2": { - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==" - }, "@esbuild/freebsd-arm64@0.25.0": { "integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==" }, - "@esbuild/freebsd-x64@0.24.2": { - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==" - }, "@esbuild/freebsd-x64@0.25.0": { "integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==" }, - "@esbuild/linux-arm64@0.24.2": { - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==" - }, "@esbuild/linux-arm64@0.25.0": { "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": { "integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==" }, - "@esbuild/linux-ia32@0.24.2": { - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==" - }, "@esbuild/linux-ia32@0.25.0": { "integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==" }, - "@esbuild/linux-loong64@0.24.2": { - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==" - }, "@esbuild/linux-loong64@0.25.0": { "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": { "integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==" }, - "@esbuild/linux-ppc64@0.24.2": { - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==" - }, "@esbuild/linux-ppc64@0.25.0": { "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": { "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": { "integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==" }, - "@esbuild/linux-x64@0.24.2": { - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==" - }, "@esbuild/linux-x64@0.25.0": { "integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==" }, - "@esbuild/netbsd-arm64@0.24.2": { - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==" - }, "@esbuild/netbsd-arm64@0.25.0": { "integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==" }, - "@esbuild/netbsd-x64@0.24.2": { - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==" - }, "@esbuild/netbsd-x64@0.25.0": { "integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==" }, - "@esbuild/openbsd-arm64@0.24.2": { - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==" - }, "@esbuild/openbsd-arm64@0.25.0": { "integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==" }, - "@esbuild/openbsd-x64@0.24.2": { - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==" - }, "@esbuild/openbsd-x64@0.25.0": { "integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==" }, - "@esbuild/sunos-x64@0.24.2": { - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==" - }, "@esbuild/sunos-x64@0.25.0": { "integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==" }, - "@esbuild/win32-arm64@0.24.2": { - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==" - }, "@esbuild/win32-arm64@0.25.0": { "integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==" }, - "@esbuild/win32-ia32@0.24.2": { - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==" - }, "@esbuild/win32-ia32@0.25.0": { "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": { "integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==" }, @@ -1099,7 +1008,7 @@ "dependencies": [ "@inquirer/core", "@inquirer/type", - "@types/node@22.13.8" + "@types/node" ] }, "@inquirer/core@10.1.7_@types+node@22.13.8": { @@ -1107,7 +1016,7 @@ "dependencies": [ "@inquirer/figures", "@inquirer/type", - "@types/node@22.13.8", + "@types/node", "ansi-escapes", "cli-width", "mute-stream", @@ -1122,7 +1031,7 @@ "@inquirer/type@3.0.4_@types+node@22.13.8": { "integrity": "sha512-2MNFrDY8jkFYc9Il9DgLsHhMzuHnOYM1+CUYVWbzu9oT0hC7V7EcYvdCKeoll/Fcci04A+ERZ9wcc7cQ8lTkIA==", "dependencies": [ - "@types/node@22.13.8" + "@types/node" ] }, "@isaacs/cliui@8.0.2": { @@ -1305,29 +1214,12 @@ "@open-draft/until@2.1.0": { "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": { "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==" }, "@polka/url@1.0.0-next.28": { "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": { "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==" }, @@ -3531,16 +3423,10 @@ "@types/pbf" ] }, - "@types/node@20.17.22": { - "integrity": "sha512-9RV2zST+0s3EhfrMZIhrz2bhuhBwxgkbHEwP2gtGWPjBzVQjifMzJ9exw7aDZhR1wbpj8zBrfp3bo8oJcGiUUw==", - "dependencies": [ - "undici-types@6.19.8" - ] - }, "@types/node@22.13.8": { "integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==", "dependencies": [ - "undici-types@6.20.0" + "undici-types" ] }, "@types/pbf@3.0.5": { @@ -3848,9 +3734,6 @@ "bignumber.js@9.1.2": { "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==" }, - "bippy@0.2.7": { - "integrity": "sha512-LTCos3SmOJHrag0qF91tLUZMMw6wA+i15ESRBp71pvfNlTMYcxYoJHJ/pvFhd+29Wm5vfgVxBHV7kP5OKUUipg==" - }, "bn.js@4.12.1": { "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" }, @@ -4458,64 +4341,34 @@ "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": { "integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==", "dependencies": [ - "@esbuild/aix-ppc64@0.25.0", - "@esbuild/android-arm@0.25.0", - "@esbuild/android-arm64@0.25.0", - "@esbuild/android-x64@0.25.0", - "@esbuild/darwin-arm64@0.25.0", - "@esbuild/darwin-x64@0.25.0", - "@esbuild/freebsd-arm64@0.25.0", - "@esbuild/freebsd-x64@0.25.0", - "@esbuild/linux-arm@0.25.0", - "@esbuild/linux-arm64@0.25.0", - "@esbuild/linux-ia32@0.25.0", - "@esbuild/linux-loong64@0.25.0", - "@esbuild/linux-mips64el@0.25.0", - "@esbuild/linux-ppc64@0.25.0", - "@esbuild/linux-riscv64@0.25.0", - "@esbuild/linux-s390x@0.25.0", - "@esbuild/linux-x64@0.25.0", - "@esbuild/netbsd-arm64@0.25.0", - "@esbuild/netbsd-x64@0.25.0", - "@esbuild/openbsd-arm64@0.25.0", - "@esbuild/openbsd-x64@0.25.0", - "@esbuild/sunos-x64@0.25.0", - "@esbuild/win32-arm64@0.25.0", - "@esbuild/win32-ia32@0.25.0", - "@esbuild/win32-x64@0.25.0" + "@esbuild/aix-ppc64", + "@esbuild/android-arm", + "@esbuild/android-arm64", + "@esbuild/android-x64", + "@esbuild/darwin-arm64", + "@esbuild/darwin-x64", + "@esbuild/freebsd-arm64", + "@esbuild/freebsd-x64", + "@esbuild/linux-arm", + "@esbuild/linux-arm64", + "@esbuild/linux-ia32", + "@esbuild/linux-loong64", + "@esbuild/linux-mips64el", + "@esbuild/linux-ppc64", + "@esbuild/linux-riscv64", + "@esbuild/linux-s390x", + "@esbuild/linux-x64", + "@esbuild/netbsd-arm64", + "@esbuild/netbsd-x64", + "@esbuild/openbsd-arm64", + "@esbuild/openbsd-x64", + "@esbuild/sunos-x64", + "@esbuild/win32-arm64", + "@esbuild/win32-ia32", + "@esbuild/win32-x64" ] }, "escalade@3.2.0": { @@ -4701,12 +4554,6 @@ "get-intrinsic" ] }, - "get-tsconfig@4.10.0": { - "integrity": "sha512-kGzZ3LWWQcGIAmg6iWvXn0ei6WDtV26wzHRMwDSzmAbcXrTEXxHy6IehI6/4eT6VRKyMP1eF1VqwrVUmE/LR7A==", - "dependencies": [ - "resolve-pkg-maps" - ] - }, "get-value@2.0.6": { "integrity": "sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==" }, @@ -5139,9 +4986,6 @@ "kind-of@6.0.3": { "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==" }, - "kleur@4.1.5": { - "integrity": "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ==" - }, "leven@3.1.0": { "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==" }, @@ -5340,9 +5184,6 @@ "mkdirp@3.0.1": { "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==" }, - "mri@1.2.0": { - "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==" - }, "mrmime@2.0.1": { "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==" }, @@ -5607,9 +5448,6 @@ "potpack@2.0.0": { "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": { "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==" }, @@ -5777,31 +5615,6 @@ "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": { "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", "dependencies": [ @@ -5912,9 +5725,6 @@ "requires-port@1.0.0": { "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" }, - "resolve-pkg-maps@1.0.0": { - "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==" - }, "resolve-protobuf-schema@2.1.0": { "integrity": "sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==", "dependencies": [ @@ -6153,9 +5963,6 @@ "totalist" ] }, - "sisteransi@1.0.5": { - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" - }, "skmeans@0.9.7": { "integrity": "sha512-hNj1/oZ7ygsfmPZ7ZfN5MUBRoGg1gtpnImuJBgLO0ljQ67DtJuiQaiYdS4lUA6s0KCwnPhGivtC/WRwIZLkHyg==" }, @@ -6516,14 +6323,6 @@ "tslog@4.9.3": { "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": { "integrity": "sha512-C3TaO7K81YvjCgQH9Q1S3R3P3BtN3RIM8n+OvX4il1K1zgE8ZhI0op7kClgkxtutIE8hQrcrHBXvIheqKUUCxw==" }, @@ -6601,9 +6400,6 @@ "which-boxed-primitive" ] }, - "undici-types@6.19.8": { - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" - }, "undici-types@6.20.0": { "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" }, @@ -6644,13 +6440,6 @@ "universalify@2.0.1": { "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==" }, - "unplugin@2.1.0": { - "integrity": "sha512-us4j03/499KhbGP8BU7Hrzrgseo+KdfJYWcbcajCOqsAyb8Gk0Yn2kiUIcZISYCb1JFaZfIuG3b42HmguVOKCQ==", - "dependencies": [ - "acorn", - "webpack-virtual-modules" - ] - }, "upath@1.2.0": { "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==" }, @@ -6747,8 +6536,8 @@ "vite@6.2.0_@types+node@22.13.8": { "integrity": "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ==", "dependencies": [ - "@types/node@22.13.8", - "esbuild@0.25.0", + "@types/node", + "esbuild", "fsevents@2.3.3", "postcss", "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": { "integrity": "sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA==", "dependencies": [ - "@types/node@22.13.8", + "@types/node", "@vitest/browser", "@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", @@ -6799,9 +6588,6 @@ "webidl-conversions@7.0.0": { "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": { "integrity": "sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==" }, @@ -7151,7 +6937,6 @@ "npm:react-hook-form@^7.54.2", "npm:react-map-gl@8.0.1", "npm:react-qrcode-logo@3", - "npm:react-scan@~0.2.8", "npm:react@19", "npm:rfc4648@^1.5.4", "npm:simple-git-hooks@^2.11.1", diff --git a/package.json b/package.json index 1bea604b..51b99b39 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "format": "deno fmt src/", "dev": "deno task dev:ui", "dev:ui": "deno run -A npm:vite dev", - "dev:scan": "VITE_DEBUG_SCAN=true deno task dev:ui", "test": "deno run -A npm:vitest", "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/ ." @@ -72,10 +71,8 @@ "react-hook-form": "^7.54.2", "react-map-gl": "8.0.1", "react-qrcode-logo": "^3.0.0", - "react-scan": "^0.2.8", "rfc4648": "^1.5.4", "vite-plugin-node-polyfills": "^0.23.0", - "zustand": "5.0.3" }, "devDependencies": { diff --git a/src/components/Dialog/DialogManager.tsx b/src/components/Dialog/DialogManager.tsx index e8d597d4..1cfbcb3e 100644 --- a/src/components/Dialog/DialogManager.tsx +++ b/src/components/Dialog/DialogManager.tsx @@ -6,8 +6,9 @@ import { PkiBackupDialog } from "@components/Dialog/PKIBackupDialog.tsx"; import { QRDialog } from "@components/Dialog/QRDialog.tsx"; import { RebootDialog } from "@components/Dialog/RebootDialog.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 { RefreshKeysDialog } from "@components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx"; export const DialogManager = () => { const { channels, config, dialog, setDialogOpen } = useDevice(); @@ -70,6 +71,12 @@ export const DialogManager = () => { setDialogOpen("unsafeRoles", open); }} /> + { + setDialogOpen("refreshKeys", open); + }} + /> ); }; diff --git a/src/components/Dialog/NewDeviceDialog.tsx b/src/components/Dialog/NewDeviceDialog.tsx index 8e755e4d..57cca565 100644 --- a/src/components/Dialog/NewDeviceDialog.tsx +++ b/src/components/Dialog/NewDeviceDialog.tsx @@ -53,7 +53,7 @@ const links: { [key: string]: string } = { const listFormatter = new Intl.ListFormat("en", { style: "long", - type: "conjunction", + type: "disjunction", }); const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => { @@ -79,16 +79,16 @@ const ErrorMessage = ({ missingFeatures }: FeatureErrorProps) => { }; return ( - +
- +
-

+

{browserFeatures.length > 0 && ( <> - This application requires{" "} + This connection type requires{" "} {formatFeatureList(browserFeatures)}. Please use a - Chromium-based browser like Chrome or Edge. + supported browser, like Chrome or Edge. )} {needsSecureContext && ( diff --git a/src/components/Dialog/NodeDetailsDialog.tsx b/src/components/Dialog/NodeDetailsDialog.tsx deleted file mode 100644 index 2f90bae2..00000000 --- a/src/components/Dialog/NodeDetailsDialog.tsx +++ /dev/null @@ -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 - ? ( -

- - - - - Node Details for {device.user?.longName ?? "UNKNOWN"} ( - {device.user?.shortName ?? "UNK"}) - - - -
- -
-

- Details: -

-

- Hardware:{" "} - {Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]} -

-

Node Number: {device.num}

-

Node HEX: !{numberToHexUnpadded(device.num)}

-

- Role: {Protobuf.Config.Config_DeviceConfig_Role[ - device.user?.role ?? 0 - ]} -

-

- Last Heard: {device.lastHeard === 0 - ? ( - "Never" - ) - : } -

-
- - {device.position - ? ( -
-

- Position: -

- {device.position.latitudeI && device.position.longitudeI - ? ( -

- Coordinates:{" "} - - {device.position.latitudeI / 1e7},{" "} - {device.position.longitudeI / 1e7} - -

- ) - : null} - {device.position.altitude - ?

Altitude: {device.position.altitude}m

- : null} -
- ) - : null} - - {device.deviceMetrics - ? ( -
-

- Device Metrics: -

- {device.deviceMetrics.airUtilTx - ? ( -

- Air TX utilization:{" "} - {device.deviceMetrics.airUtilTx.toFixed(2)}% -

- ) - : null} - {device.deviceMetrics.channelUtilization - ? ( -

- Channel utilization:{" "} - {device.deviceMetrics.channelUtilization.toFixed(2)}% -

- ) - : null} - {device.deviceMetrics.batteryLevel - ? ( -

- Battery level:{" "} - {device.deviceMetrics.batteryLevel.toFixed(2)}% -

- ) - : null} - {device.deviceMetrics.voltage - ? ( -

- Voltage: {device.deviceMetrics.voltage.toFixed(2)}V -

- ) - : null} - {device.deviceMetrics.uptimeSeconds - ? ( -

- Uptime:{" "} - -

- ) - : null} -
- ) - : null} - - {device - ? ( -
- - - -

- All Raw Metrics: -

-
- -
-                            {JSON.stringify(device, null, 2)}
-                          
-
-
-
-
- ) - : null} -
-
-
-
- ) - : null; -}; diff --git a/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.test.tsx b/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.test.tsx new file mode 100644 index 00000000..d8d1bea9 --- /dev/null +++ b/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.test.tsx @@ -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( { }} />); + + 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( { }} />); + expect(screen.queryByText(/Node Details for/i)).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx b/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx new file mode 100644 index 00000000..f010fc67 --- /dev/null +++ b/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx @@ -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 ( + + + + + + Node Details for {device.user?.longName ?? "UNKNOWN"} ( + {device.user?.shortName ?? "UNK"}) + + + +
+
+ +
+

Details:

+

+ Hardware:{" "} + {Protobuf.Mesh.HardwareModel[device.user?.hwModel ?? 0]} +

+

Node Number: {device.num}

+

Node Hex: !{numberToHexUnpadded(device.num)}

+

+ Role:{" "} + { + Protobuf.Config.Config_DeviceConfig_Role[ + device.user?.role ?? 0 + ] + } +

+

+ Last Heard:{" "} + {device.lastHeard === 0 ? "Never" : } +

+
+ + {device.position && ( +
+

Position:

+ {device.position.latitudeI && device.position.longitudeI && ( +

+ Coordinates:{" "} + + {device.position.latitudeI / 1e7},{" "} + {device.position.longitudeI / 1e7} + +

+ )} + {device.position.altitude && ( +

Altitude: {device.position.altitude}m

+ )} +
+ )} + + {device.deviceMetrics && ( +
+

+ Device Metrics: +

+ {deviceMetricsMap.map( + (metric) => + metric.value !== undefined && ( +

+ {metric.label}: {metric.format(metric.value)} +

+ ) + )} + {device.deviceMetrics.uptimeSeconds && ( +

+ Uptime:{" "} + +

+ )} +
+ )} + +
+ +
+ + + +

+ All Raw Metrics: +

+
+ +
+                      {JSON.stringify(device, null, 2)}
+                    
+
+
+
+
+
+
+
+
+ ); +}; diff --git a/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.test.tsx b/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.test.tsx new file mode 100644 index 00000000..e955b88f --- /dev/null +++ b/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.test.tsx @@ -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(); + 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(); + fireEvent.click(screen.getByText("Request New Keys")); + expect(handleNodeRemoveMock).toHaveBeenCalled(); + }); + + it("calls handleCloseDialog when 'Dismiss' button is clicked", () => { + render(); + fireEvent.click(screen.getByText("Dismiss")); + expect(handleCloseDialogMock).toHaveBeenCalled(); + }); + + it("calls onOpenChange when dialog close button is clicked", () => { + render(); + fireEvent.click(screen.getByRole("button", { name: /close/i })); + expect(handleCloseDialogMock).toHaveBeenCalled(); + }); + + it("does not render when open is false", () => { + render(); + expect(screen.queryByText("Keys Mismatch")).not.toBeInTheDocument(); + }); +}); diff --git a/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx b/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx new file mode 100644 index 00000000..d2fc659d --- /dev/null +++ b/src/components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx @@ -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 ( + + + + + Keys Mismatch + + 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. +
    +
  • +
    + +
    +
    +
    +

    Accept New Keys

    +

    + This will remove the node from device and request new keys. +

    +
    + + +
    +
  • +
+ {/* */} +
+
+ ); +}; diff --git a/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.test.ts b/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.test.ts new file mode 100644 index 00000000..b26d0165 --- /dev/null +++ b/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.test.ts @@ -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); + }); +}); diff --git a/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts b/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts new file mode 100644 index 00000000..821aade7 --- /dev/null +++ b/src/components/Dialog/RefreshKeysDialog/useRefreshKeysDialog.ts @@ -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 + }; + +} \ No newline at end of file diff --git a/src/components/Form/DynamicForm.tsx b/src/components/Form/DynamicForm.tsx index 00d26c4f..5522ec4f 100644 --- a/src/components/Form/DynamicForm.tsx +++ b/src/components/Form/DynamicForm.tsx @@ -124,7 +124,7 @@ export function DynamicForm({ })}
))} - {hasSubmitButton && } + {hasSubmitButton && } ); } diff --git a/src/components/PageComponents/Channel.tsx b/src/components/PageComponents/Channel.tsx index 1104c4c1..1356f616 100644 --- a/src/components/PageComponents/Channel.tsx +++ b/src/components/PageComponents/Channel.tsx @@ -34,12 +34,8 @@ export const Channel = ({ channel }: SettingsPanelProps) => { settings: { ...data.settings, psk: toByteArray(pass), - moduleSettings: { - positionPrecision: data.settings.positionEnabled - ? data.settings.preciseLocation - ? 32 - : data.settings.positionPrecision - : 0, + moduleSettings: {...data.settings.moduleSettings, + positionPrecision: data.settings.moduleSettings.positionPrecision, }, }, }); @@ -100,17 +96,9 @@ export const Channel = ({ channel }: SettingsPanelProps) => { settings: { ...channel?.settings, psk: pass, - positionEnabled: - 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, + moduleSettings: {...channel?.settings?.moduleSettings, + positionPrecision: channel?.settings?.moduleSettings?.positionPrecision === undefined ? 10 : channel?.settings?.moduleSettings?.positionPrecision, + } }, }, }} @@ -174,39 +162,30 @@ export const Channel = ({ channel }: SettingsPanelProps) => { label: "Downlink Enabled", 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", - name: "settings.positionPrecision", - label: "Approximate Location", + name: "settings.moduleSettings.positionPrecision", + label: "Location", 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: { enumValue: config.display?.units === 0 ? { - "Within 23 km": 10, - "Within 12 km": 11, - "Within 5.8 km": 12, - "Within 2.9 km": 13, - "Within 1.5 km": 14, - "Within 700 m": 15, - "Within 350 m": 16, - "Within 200 m": 17, - "Within 90 m": 18, - "Within 50 m": 19, + "Do not share location": 0, + "Within 23 kilometers": 10, + "Within 12 kilometers": 11, + "Within 5.8 kilometers": 12, + "Within 2.9 kilometers": 13, + "Within 1.5 kilometers": 14, + "Within 700 meters": 15, + "Within 350 meters": 16, + "Within 200 meters": 17, + "Within 90 meters": 18, + "Within 50 meters": 19, + "Precise Location": 32, } : { + "Do not share location": 0, "Within 15 miles": 10, "Within 7.3 miles": 11, "Within 3.6 miles": 12, @@ -217,6 +196,7 @@ export const Channel = ({ channel }: SettingsPanelProps) => { "Within 600 feet": 17, "Within 300 feet": 18, "Within 150 feet": 19, + "Precise Location": 32, }, }, }, diff --git a/src/components/PageComponents/Connect/Serial.tsx b/src/components/PageComponents/Connect/Serial.tsx index b6cdf5f9..28085cc2 100644 --- a/src/components/PageComponents/Connect/Serial.tsx +++ b/src/components/PageComponents/Connect/Serial.tsx @@ -18,9 +18,7 @@ export const Serial = ({ closeDialog }: TabElementProps) => { setSerialPorts(await navigator?.serial.getPorts()); }, []); - navigator?.serial?.addEventListener("connect", (event) => { - console.log(event); - + navigator?.serial?.addEventListener("connect", () => { updateSerialPortList(); }); navigator?.serial?.addEventListener("disconnect", () => { @@ -47,8 +45,6 @@ export const Serial = ({ closeDialog }: TabElementProps) => {
{serialPorts.map((port, index) => { - console.log(port); - const { usbProductId, usbVendorId } = port.getInfo(); return ( ); diff --git a/src/components/UI/Avatar.tsx b/src/components/UI/Avatar.tsx index 4b8b94cb..484aa2c3 100644 --- a/src/components/UI/Avatar.tsx +++ b/src/components/UI/Avatar.tsx @@ -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"; type RGBColor = { @@ -12,6 +13,7 @@ interface AvatarProps { text: string; size?: "sm" | "lg"; className?: string; + showError?: boolean; } // biome-ignore lint/complexity/noStaticOnlyClass: stop being annoying Biome @@ -43,6 +45,7 @@ class ColorUtils { export const Avatar: React.FC = ({ text, size = "sm", + showError = false, className, }) => { const sizes = { @@ -73,12 +76,11 @@ export const Avatar: React.FC = ({ return (
= ({ color: textColor, }} > + {showError ? : null}

{initials}

); diff --git a/src/components/UI/Button.tsx b/src/components/UI/Button.tsx index 08c7a7a8..bb7e68e1 100644 --- a/src/components/UI/Button.tsx +++ b/src/components/UI/Button.tsx @@ -15,7 +15,7 @@ const buttonVariants = cva( success: "bg-green-500 text-white hover:bg-green-600 dark:hover:bg-green-600", 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: "bg-slate-100 text-slate-700 hover:bg-slate-200 dark:bg-slate-500 dark:text-white dark:hover:bg-slate-400", ghost: diff --git a/src/components/generic/Table/index.tsx b/src/components/generic/Table/index.tsx index ebd5bae4..1603fd00 100755 --- a/src/components/generic/Table/index.tsx +++ b/src/components/generic/Table/index.tsx @@ -92,7 +92,7 @@ export const Table = ({ headings, rows }: TableProps) => { { {sortedRows.map((row, index) => ( // biome-ignore lint/suspicious/noArrayIndexKey: TODO: Once this table is sortable, this should get fixed. - + {row.map((item, index) => ( + index === 0 ? + + {item} + : [] >; + nodeErrors: Map; connection?: MeshDevice; activePage: Page; activeNode: number; @@ -71,6 +78,7 @@ export interface Device { pkiBackup: boolean; nodeDetails: boolean; unsafeRoles: boolean; + refreshKeys: boolean; }; unreadCounts: Map; @@ -111,6 +119,10 @@ export interface Device { setMessageDraft: (message: string) => void; setUnread: (id: number, count: number) => 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 { @@ -164,10 +176,12 @@ export const useDeviceStore = createStore((set, get) => ({ pkiBackup: false, nodeDetails: false, unsafeRoles: false, + refreshKeys: false, }, pendingSettingsChanges: false, messageDraft: "", unreadCounts: new Map(), + nodeErrors: new Map(), setStatus: (status: Types.DeviceStatusEnum) => { set( @@ -534,7 +548,6 @@ export const useDeviceStore = createStore((set, get) => ({ addTraceRoute: (traceroute) => { set( produce((draft) => { - console.log("addTraceRoute called"); const device = draft.devices.get(id); if (!device) { return; @@ -571,10 +584,8 @@ export const useDeviceStore = createStore((set, get) => ({ ) => { set( produce((draft) => { - console.log("setMessageState called"); const device = draft.devices.get(id); if (!device) { - console.log("no device found for id"); return; } const messageGroup = device.messages[type]; @@ -585,7 +596,6 @@ export const useDeviceStore = createStore((set, get) => ({ const messages = messageGroup.get(messageIndex); if (!messages) { - console.log("no messages found for id"); return; } @@ -680,7 +690,42 @@ export const useDeviceStore = createStore((set, get) => ({ } }), ); - } + }, + setNodeError: (nodeNum, error) => { + set( + produce((draft) => { + const device = draft.devices.get(id); + if (device) { + device.nodeErrors.set(nodeNum, { node: nodeNum, error }); + } + }), + ); + }, + clearNodeError: (nodeNum: number) => { + set( + produce((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); + }, + }); }), ); diff --git a/src/core/subscriptions.ts b/src/core/subscriptions.ts index db52de3e..355d082b 100644 --- a/src/core/subscriptions.ts +++ b/src/core/subscriptions.ts @@ -23,15 +23,15 @@ export const subscribeAll = ( ) { return; } - console.log(`Routing Error: ${routingPacket.data.variant.value}`); + console.info(`Routing Error: ${routingPacket.data.variant.value}`); break; } case "routeReply": { - console.log(`Route Reply: ${routingPacket.data.variant.value}`); + console.info(`Route Reply: ${routingPacket.data.variant.value}`); break; } case "routeRequest": { - console.log(`Route Request: ${routingPacket.data.variant.value}`); + console.info(`Route Request: ${routingPacket.data.variant.value}`); break; } } @@ -71,8 +71,6 @@ export const subscribeAll = ( }); connection.events.onChannelPacket.subscribe((channel) => { - console.log('channel', channel); - device.addChannel(channel); }); connection.events.onConfigPacket.subscribe((config) => { @@ -82,10 +80,8 @@ export const subscribeAll = ( device.setModuleConfig(moduleConfig); }); - connection.events.onMessagePacket.subscribe((messagePacket) => { - - console.log('messagePacket', messagePacket); + connection.events.onMessagePacket.subscribe((messagePacket) => { device.addMessage({ ...messagePacket, state: messagePacket.from !== myNodeNum ? "ack" : "waiting", @@ -120,8 +116,26 @@ export const subscribeAll = ( connection.events.onQueueStatus.subscribe((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; + } + } + } }); }; diff --git a/src/index.tsx b/src/index.tsx index 7b56e632..33d79536 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,3 @@ -import { scan } from "react-scan"; import "@app/index.css"; import { enableMapSet } from "immer"; import "maplibre-gl/dist/maplibre-gl.css"; @@ -7,13 +6,6 @@ import { createRoot } from "react-dom/client"; 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 root = createRoot(container); diff --git a/src/pages/Messages.tsx b/src/pages/Messages.tsx index 27cf98c9..b59afe72 100644 --- a/src/pages/Messages.tsx +++ b/src/pages/Messages.tsx @@ -13,9 +13,10 @@ import { getChannelName } from "@pages/Channels.tsx"; import { HashIcon, LockIcon, LockOpenIcon } from "lucide-react"; import { useState } from "react"; import { MessageInput } from "@components/PageComponents/Messages/MessageInput.tsx"; +import { cn } from "@core/utils/cn.ts"; 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 [searchTerm, setSearchTerm] = useState(""); const filteredNodes = Array.from(nodes.values()).filter((node) => { @@ -91,6 +92,8 @@ export const MessagesPage = () => { element={ } @@ -154,7 +157,6 @@ export const MessagesPage = () => { )}
- {/* Single message input for both chat types */}
void; } +function shortNameFromNode( + node: ReturnType["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 { nodes, hardware, connection } = useDevice(); - console.log(connection); - const [selectedNode, setSelectedNode] = useState< Protobuf.Mesh.NodeInfo | undefined >(undefined); @@ -63,7 +75,6 @@ const NodesPage = (): JSX.Element => { }; }, [connection]); - const handleLocation = useCallback( (location: Types.PacketMetadata) => { if (location.to.valueOf() !== hardware.myNodeNum) return; @@ -89,82 +100,70 @@ const NodesPage = (): JSX.Element => { [
- +
, - -

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)}`)} -

, -

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?.macaddr - ? `Meshtastic ${base16 - .stringify(node.user?.macaddr.subarray(4, 6) ?? []) - .toLowerCase()}` + ? `Meshtastic ${ + base16 + .stringify(node.user?.macaddr.subarray(4, 6) ?? []) + .toLowerCase() + }` : `!${numberToHexUnpadded(node.num)}`)}

, - - - {Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]} + + {node.lastHeard !== 0 + ? node.viaMqtt === false && node.hopsAway === 0 + ? "Direct" + : `${node.hopsAway?.toString()} ${ + node.hopsAway > 1 ? "hops" : "hop" + } away` + : "-"} + {node.viaMqtt === true ? ", via MQTT" : ""} , - - {base16 - .stringify(node.user?.macaddr ?? []) - .match(/.{1,2}/g) - ?.join(":") ?? "UNK"} + + {node.lastHeard === 0 + ?

Never

+ : }
, - - {node.lastHeard === 0 ? ( -

Never

- ) : ( - - )} + + {node.user?.publicKey && node.user?.publicKey.length > 0 + ? + : } , {node.snr}db/ {Math.min(Math.max((node.snr + 10) * 5, 0), 100)}%/ {(node.snr + 10) * 5}raw , - - {node.user?.publicKey && node.user?.publicKey.length > 0 ? ( - - ) : ( - - )} + + {Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]} , - - {node.lastHeard !== 0 - ? node.viaMqtt === false && node.hopsAway === 0 - ? "Direct" - : `${node.hopsAway?.toString()} ${node.hopsAway > 1 ? "hops" : "hop" - } away` - : "-"} - {node.viaMqtt === true ? ", via MQTT" : ""} + + {base16 + .stringify(node.user?.macaddr ?? []) + .match(/.{1,2}/g) + ?.join(":") ?? "UNK"} , ])} /> diff --git a/vite.config.ts b/vite.config.ts index b6b90406..3471597e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -44,11 +44,8 @@ export default defineConfig({ server: { port: 3000, headers: { - 'Cross-Origin-Opener-Policy': 'same-origin', - 'Cross-Origin-Embedder-Policy': 'require-corp', - } - }, - optimizeDeps: { - exclude: ['react-scan'] + "Cross-Origin-Opener-Policy": "same-origin", + "Cross-Origin-Embedder-Policy": "require-corp", + }, }, }); \ No newline at end of file