55 Commits
Author SHA1 Message Date
Hunter ThornsberryandGitHub 3fe38eb506 Merge pull request #295 from meshtastic/master
Update release.yml
2024-09-10 20:53:31 -04:00
Hunter ThornsberryandGitHub 51081d3052 Update release.yml
Create a build artifact on release
2024-09-10 20:51:50 -04:00
Hunter Thornsberry c08f6d16bb Merge branch 'master' into pki 2024-09-09 18:55:02 -04:00
Hunter ThornsberryandGitHub 62ad4c49f8 Merge pull request #293 from Hunter275/pki-nodelist
Node List & DMs
2024-09-09 18:50:29 -04:00
Hunter Thornsberry 3b0a1e6108 biome 2024-09-09 18:37:22 -04:00
Hunter Thornsberry c2f2205626 cleanup 2024-09-09 18:25:58 -04:00
Hunter ThornsberryandGitHub 87c729d694 Merge branch 'pki' into pki-nodelist 2024-09-09 18:01:17 -04:00
Hunter Thornsberry 8e4f60edf3 biome 2024-09-09 16:36:56 -04:00
Hunter Thornsberry 8811eee9f5 Remove bluetooth debugging and reword generic debug 2024-09-09 16:36:17 -04:00
Hunter Thornsberry 2af93f1acd Fix protobufs, add configOkToMqtt, add PKI icons 2024-09-09 16:22:48 -04:00
Hunter Thornsberry 78a35544c7 Node list and DMs now show icons 2024-09-08 19:48:42 -04:00
Hunter Thornsberry 3ad2d650b0 update protobufs 2024-09-08 18:59:57 -04:00
Hunter ThornsberryandGitHub 2e12b27566 Merge pull request #277 from KomelT/feature/security-tab
Update key generation
2024-09-08 18:13:42 -04:00
Hunter Thornsberry 989fad7e17 biome 2024-09-07 20:24:45 -04:00
Hunter Thornsberry f7a2e5f76b Invert adminKey disabledBy 2024-09-07 20:24:18 -04:00
Hunter Thornsberry 2b81fc47e2 disable the public key input 2024-09-07 20:19:51 -04:00
Hunter Thornsberry b522113cd7 bad merge on Security.tsx 2024-09-07 20:16:29 -04:00
Hunter ThornsberryandGitHub fce642c24e Merge branch 'pki' into feature/security-tab 2024-09-07 19:54:57 -04:00
Hunter ThornsberryandGitHub bf425a8ec7 Merge pull request #291 from Kongduino/patch-1 2024-09-07 12:21:47 -04:00
KongduinoandGitHub a7d0d36086 Update index.tsx
"at least", two words. Thanks...
2024-09-07 13:32:16 +08:00
Tilen Komel 5e72510bef Removed submit button 2024-09-06 09:33:13 +02:00
Hunter Thornsberry 88efdc4758 Merge branch 'master' into pki 2024-09-05 19:26:32 -04:00
Hunter ThornsberryandGitHub 41acc4d25d Merge pull request #289 from Hunter275/gh-actions
GitHub Actions: Releases
2024-09-04 16:15:41 -04:00
Tilen Komel d10c010b9a Dont disable PKI on Allow Legacy Admin 2024-09-04 18:50:49 +02:00
Hunter Thornsberry 80ab44c8db don't release on release 2024-09-04 00:39:12 -04:00
Hunter Thornsberry 7895df2d9f Stop release on ci 2024-09-04 00:35:22 -04:00
Hunter ThornsberryandGitHub c780437355 Merge pull request #286 from TheCyberRonin/feature/provide-compressed-build-during-pr
feat: Provide compressed build during PR GitHub Action
2024-09-04 00:12:26 -04:00
Tilen Komel 9d4aa05316 Updated pki regenerate dialog 2024-09-02 15:45:58 +02:00
Tilen Komel 354d04592b Fix build 2024-09-02 15:05:04 +02:00
Tilen Komel 9bea6870bb Add submit button to work 2024-09-02 13:11:42 +02:00
Tilen Komel d0bd02980d Update private key generation 2024-09-02 13:10:59 +02:00
Hunter ThornsberryandGitHub 7d1135b9dc Merge pull request #288 from mrfyda/master
Change 'powerScreenEnabled' config type to toggle
2024-09-01 19:00:25 -04:00
Tilen Komel f66332b3e3 Handle undefined booleans 2024-08-31 15:58:12 +02:00
Rafael Cortês 05a6b6293e Change 'powerScreenEnabled' config type to toggle 2024-08-31 10:51:53 +01:00
Hunter ThornsberryandGitHub b9a8a2ba6c Merge pull request #287 from TheCyberRonin/feature/scope-ci-to-push-on-master
feat: Only do `CI` GHA on push to `master`
2024-08-30 12:31:09 -04:00
Ronin c16070f02b feat: Only do CI GHA on push to master 2024-08-29 21:54:58 -04:00
Ronin 3e6653a98f chore: Fix typo in package name for uploading 2024-08-29 21:31:04 -04:00
Ronin 59126ca939 feat: Add a build step for build.tar 2024-08-29 21:27:37 -04:00
Tilen Komel 4bde402a53 Add key generation confirmation 2024-08-29 23:19:12 +02:00
Tilen Komel 4cf91272de Removed admin key generation from security 2024-08-29 23:19:12 +02:00
Tilen Komel c711c39aa9 Add inputChange to FormInpit 2024-08-29 23:19:12 +02:00
Tilen Komel c4342f1a2b Update passwordGenerator disabled 2024-08-29 23:19:12 +02:00
Tilen Komel afc45588fa Fixed security key generation 2024-08-29 23:19:12 +02:00
Tilen Komel 22cd5aa88d Add key generation 2024-08-29 23:19:12 +02:00
Tilen Komel 8c4c8a760e Add @noble/curves 2024-08-29 23:19:11 +02:00
Tilen Komel 0911df6b0d Add dynamic bit to password generator 2024-08-29 23:16:02 +02:00
Tilen Komel 06c20fa950 Add dynamic value to input 2024-08-29 23:16:02 +02:00
Hunter ThornsberryandGitHub 22d900a831 Merge pull request #278 from Hunter275/dashboard-footer
flex-grow to make footer stick to bottom
2024-08-26 15:33:40 -04:00
Hunter Thornsberry c6cc5e5e6f biome 2024-08-26 14:32:18 -04:00
Hunter Thornsberry f0d8db1c87 flex-grow to make footer stick to bottom 2024-08-26 14:10:43 -04:00
Hunter ThornsberryandGitHub 8c37be4af3 Merge pull request #274 from rcarteraz/add-footer 2024-08-24 11:50:51 -04:00
Hunter Thornsberry 076dae80b7 first working version 2024-08-23 18:58:22 -04:00
Hunter Thornsberry 7d4001ea9d Update footer and initial groundword 2024-08-23 18:40:17 -04:00
rcarteraz 6c1f140ad1 oops -- helps if you use the full link 2024-08-19 13:31:22 -07:00
rcarteraz 4c4be2e18f temp solution 2024-08-19 13:20:56 -07:00
23 changed files with 764 additions and 461 deletions
+4 -40
View File
@@ -1,6 +1,9 @@
name: CI name: CI
on: push on:
push:
branches:
- master
permissions: permissions:
contents: write contents: write
@@ -21,42 +24,3 @@ jobs:
- name: Build Package - name: Build Package
run: pnpm build run: pnpm build
- name: Package Output
run: pnpm package
- name: Upload Artifact
uses: "marvinpinto/action-automatic-releases@latest"
with:
repo_token: "${{ secrets.GITHUB_TOKEN }}"
automatic_release_tag: "latest"
prerelease: false
files: |
./dist/build.tar
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Buildah Build
id: build-container
uses: redhat-actions/buildah-build@v2
with:
containerfiles: |
./Containerfile
image: ${{github.event.repository.full_name}}
tags: latest ${{ github.sha }}
oci: true
platforms: linux/amd64, linux/arm64
- name: Push To Registry
id: push-to-registry
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ steps.build-container.outputs.image }}
tags: ${{ steps.build-container.outputs.tags }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Print image url
run: echo "Image pushed to ${{ steps.push-to-registry.outputs.registry-paths }}"
+9
View File
@@ -19,3 +19,12 @@ jobs:
- name: Build Package - name: Build Package
run: pnpm build run: pnpm build
- name: Compress build
run: pnpm package
- name: Archive compressed build
uses: actions/upload-artifact@v4
with:
name: build
path: dist/build.tar
+61
View File
@@ -0,0 +1,61 @@
name: 'Release'
on:
release:
types: [released]
permissions:
contents: write
packages: write
jobs:
build-and-package:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: latest
- name: Install Dependencies
run: pnpm install
- name: Build Package
run: pnpm build
- name: Package Output
run: pnpm package
- name: Archive compressed build
uses: actions/upload-artifact@v4
with:
name: build
path: dist/build.tar
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Buildah Build
id: build-container
uses: redhat-actions/buildah-build@v2
with:
containerfiles: |
./Containerfile
image: ${{github.event.repository.full_name}}
tags: latest ${{ github.sha }}
oci: true
platforms: linux/amd64, linux/arm64
- name: Push To Registry
id: push-to-registry
uses: redhat-actions/push-to-registry@v2
with:
image: ${{ steps.build-container.outputs.image }}
tags: ${{ steps.build-container.outputs.tags }}
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Print image url
run: echo "Image pushed to ${{ steps.push-to-registry.outputs.registry-paths }}"
+3 -2
View File
@@ -24,6 +24,7 @@
"@bufbuild/protobuf": "^1.10.0", "@bufbuild/protobuf": "^1.10.0",
"@emeraldpay/hashicon-react": "^0.5.2", "@emeraldpay/hashicon-react": "^0.5.2",
"@meshtastic/js": "2.3.7-1", "@meshtastic/js": "2.3.7-1",
"@noble/curves": "^1.5.0",
"@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-checkbox": "^1.1.0", "@radix-ui/react-checkbox": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1", "@radix-ui/react-dialog": "^1.1.1",
@@ -48,7 +49,7 @@
"crypto-random-string": "^5.0.0", "crypto-random-string": "^5.0.0",
"immer": "^10.1.1", "immer": "^10.1.1",
"lucide-react": "^0.363.0", "lucide-react": "^0.363.0",
"mapbox-gl": "npm:empty-npm-package@^1.0.0", "mapbox-gl": "^3.6.0",
"maplibre-gl": "4.1.2", "maplibre-gl": "4.1.2",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
@@ -63,7 +64,7 @@
}, },
"devDependencies": { "devDependencies": {
"@biomejs/biome": "^1.8.2", "@biomejs/biome": "^1.8.2",
"@buf/meshtastic_protobufs.bufbuild_es": "1.10.0-20240820152623-fac6975bbc78.1", "@buf/meshtastic_protobufs.bufbuild_es": "1.10.0-20240906232734-3da561588c55.1",
"@types/chrome": "^0.0.263", "@types/chrome": "^0.0.263",
"@types/node": "^20.14.9", "@types/node": "^20.14.9",
"@types/react": "^18.3.3", "@types/react": "^18.3.3",
+115 -15
View File
@@ -17,6 +17,9 @@ importers:
'@meshtastic/js': '@meshtastic/js':
specifier: 2.3.7-1 specifier: 2.3.7-1
version: 2.3.7-1 version: 2.3.7-1
'@noble/curves':
specifier: ^1.5.0
version: 1.5.0
'@radix-ui/react-accordion': '@radix-ui/react-accordion':
specifier: ^1.2.0 specifier: ^1.2.0
version: 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 1.2.0(@types/react-dom@18.3.0)(@types/react@18.3.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -90,8 +93,8 @@ importers:
specifier: ^0.363.0 specifier: ^0.363.0
version: 0.363.0(react@18.3.1) version: 0.363.0(react@18.3.1)
mapbox-gl: mapbox-gl:
specifier: npm:empty-npm-package@^1.0.0 specifier: ^3.6.0
version: empty-npm-package@1.0.0 version: 3.6.0
maplibre-gl: maplibre-gl:
specifier: 4.1.2 specifier: 4.1.2
version: 4.1.2 version: 4.1.2
@@ -106,7 +109,7 @@ importers:
version: 7.52.0(react@18.3.1) version: 7.52.0(react@18.3.1)
react-map-gl: react-map-gl:
specifier: 7.1.7 specifier: 7.1.7
version: 7.1.7(empty-npm-package@1.0.0)(maplibre-gl@4.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 7.1.7(mapbox-gl@3.6.0)(maplibre-gl@4.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-qrcode-logo: react-qrcode-logo:
specifier: ^2.10.0 specifier: ^2.10.0
version: 2.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 2.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@@ -130,8 +133,8 @@ importers:
specifier: ^1.8.2 specifier: ^1.8.2
version: 1.8.2 version: 1.8.2
'@buf/meshtastic_protobufs.bufbuild_es': '@buf/meshtastic_protobufs.bufbuild_es':
specifier: 1.10.0-20240820152623-fac6975bbc78.1 specifier: 1.10.0-20240906232734-3da561588c55.1
version: 1.10.0-20240820152623-fac6975bbc78.1(@bufbuild/protobuf@1.10.0) version: 1.10.0-20240906232734-3da561588c55.1(@bufbuild/protobuf@1.10.0)
'@types/chrome': '@types/chrome':
specifier: ^0.0.263 specifier: ^0.0.263
version: 0.0.263 version: 0.0.263
@@ -354,8 +357,8 @@ packages:
cpu: [x64] cpu: [x64]
os: [win32] os: [win32]
'@buf/meshtastic_protobufs.bufbuild_es@1.10.0-20240820152623-fac6975bbc78.1': '@buf/meshtastic_protobufs.bufbuild_es@1.10.0-20240906232734-3da561588c55.1':
resolution: {tarball: https://buf.build/gen/npm/v1/@buf/meshtastic_protobufs.bufbuild_es/-/meshtastic_protobufs.bufbuild_es-1.10.0-20240820152623-fac6975bbc78.1.tgz} resolution: {tarball: https://buf.build/gen/npm/v1/@buf/meshtastic_protobufs.bufbuild_es/-/meshtastic_protobufs.bufbuild_es-1.10.0-20240906232734-3da561588c55.1.tgz}
peerDependencies: peerDependencies:
'@bufbuild/protobuf': ^1.10.0 '@bufbuild/protobuf': ^1.10.0
@@ -557,6 +560,9 @@ packages:
resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==} resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==}
engines: {node: '>= 0.6'} engines: {node: '>= 0.6'}
'@mapbox/mapbox-gl-supported@3.0.0':
resolution: {integrity: sha512-2XghOwu16ZwPJLOFVuIOaLbN0iKMn867evzXFyf0P22dqugezfJwLmdanAgU25ITvz1TvOfVP4jsDImlDJzcWg==}
'@mapbox/point-geometry@0.1.0': '@mapbox/point-geometry@0.1.0':
resolution: {integrity: sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==} resolution: {integrity: sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==}
@@ -584,6 +590,13 @@ packages:
'@meshtastic/js@2.3.7-1': '@meshtastic/js@2.3.7-1':
resolution: {integrity: sha512-pv+Xk6HkKrScCrQp31k5QOUYozabXn6NhXN7c7Cc9ysG94U1wGtfueRbEbFxXCHO3JshNz0CdE1FcSMnrLMjsQ==} resolution: {integrity: sha512-pv+Xk6HkKrScCrQp31k5QOUYozabXn6NhXN7c7Cc9ysG94U1wGtfueRbEbFxXCHO3JshNz0CdE1FcSMnrLMjsQ==}
'@noble/curves@1.5.0':
resolution: {integrity: sha512-J5EKamIHnKPyClwVrzmaf5wSdQXgdHcPZIZLu3bwnbeCx8/7NPK5q2ZBWF+5FvYGByjiQQsJYX6jfgB2wDPn3A==}
'@noble/hashes@1.4.0':
resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==}
engines: {node: '>= 16'}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'} engines: {node: '>= 8'}
@@ -1823,6 +1836,9 @@ packages:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'} engines: {node: '>=4'}
cheap-ruler@4.0.0:
resolution: {integrity: sha512-0BJa8f4t141BYKQyn9NSQt1PguFQXMXwZiA5shfoaBYHAb2fFk2RAX+tiWMoQU+Agtzt3mdt0JtuyshAXqZ+Vw==}
chokidar@3.6.0: chokidar@3.6.0:
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
engines: {node: '>= 8.10.0'} engines: {node: '>= 8.10.0'}
@@ -1908,6 +1924,9 @@ packages:
resolution: {integrity: sha512-KWjTXWwxFd6a94m5CdRGW/t82Tr8DoBc9dNnPCAbFI1EBweN6v1tv8y4Y1m7ndkp/nkIBRxUxAzpaBnR2k3bcQ==} resolution: {integrity: sha512-KWjTXWwxFd6a94m5CdRGW/t82Tr8DoBc9dNnPCAbFI1EBweN6v1tv8y4Y1m7ndkp/nkIBRxUxAzpaBnR2k3bcQ==}
engines: {node: '>=14.16'} engines: {node: '>=14.16'}
csscolorparser@1.0.3:
resolution: {integrity: sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==}
cssesc@3.0.0: cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -1975,6 +1994,9 @@ packages:
earcut@2.2.4: earcut@2.2.4:
resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==}
earcut@3.0.0:
resolution: {integrity: sha512-41Fs7Q/PLq1SDbqjsgcY7GA42T0jvaCNGXgGtsNdvg+Yv8eIu06bxv4/PoREkZ9nMDNwnUSG9OFB9+yv8eKhDg==}
eastasianwidth@0.2.0: eastasianwidth@0.2.0:
resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==}
@@ -1987,9 +2009,6 @@ packages:
emoji-regex@9.2.2: emoji-regex@9.2.2:
resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==}
empty-npm-package@1.0.0:
resolution: {integrity: sha512-q4Mq/+XO7UNDdMiPpR/LIBIW1Zl4V0Z6UT9aKGqIAnBCtCb3lvZJM1KbDbdzdC8fKflwflModfjR29Nt0EpcwA==}
end-of-stream@1.4.4: end-of-stream@1.4.4:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
@@ -2032,6 +2051,9 @@ packages:
fastq@1.17.1: fastq@1.17.1:
resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==}
fflate@0.8.2:
resolution: {integrity: sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==}
fill-range@7.1.1: fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'} engines: {node: '>=8'}
@@ -2074,6 +2096,9 @@ packages:
geojson-vt@3.2.1: geojson-vt@3.2.1:
resolution: {integrity: sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==} resolution: {integrity: sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==}
geojson-vt@4.0.2:
resolution: {integrity: sha512-AV9ROqlNqoZEIJGfm1ncNjEXfkz2hdFlZf0qkVfmkwdKa8vj7H16YUOT81rJw1rdFhyEDlN2Tds91p/glzbl5A==}
get-caller-file@2.0.5: get-caller-file@2.0.5:
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
engines: {node: 6.* || 8.* || >= 10.*} engines: {node: 6.* || 8.* || >= 10.*}
@@ -2121,6 +2146,9 @@ packages:
gopd@1.0.1: gopd@1.0.1:
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==} resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
grid-index@1.1.0:
resolution: {integrity: sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==}
gzipper@7.2.0: gzipper@7.2.0:
resolution: {integrity: sha512-qwYQr7GWBXIm9Cdzud+tyM/s9N+QFzGDZoF9YR8RYJbDKOYowzjMDPEinFtm78EQeeYMC/FJW2FXY0bHkyUgsA==} resolution: {integrity: sha512-qwYQr7GWBXIm9Cdzud+tyM/s9N+QFzGDZoF9YR8RYJbDKOYowzjMDPEinFtm78EQeeYMC/FJW2FXY0bHkyUgsA==}
engines: {node: '>=14'} engines: {node: '>=14'}
@@ -2361,6 +2389,9 @@ packages:
peerDependencies: peerDependencies:
react: ^16.5.1 || ^17.0.0 || ^18.0.0 react: ^16.5.1 || ^17.0.0 || ^18.0.0
mapbox-gl@3.6.0:
resolution: {integrity: sha512-xjYHHIJDh6haYcKY+/9jh1eywwYfIOWCgT5Fowj4JriZexx/oOtg2S7BQDMZtpFyg9IN4VLCysmUWxY0pFNRWA==}
maplibre-gl@4.1.2: maplibre-gl@4.1.2:
resolution: {integrity: sha512-98T+3BesL4w/N39q/rgs9q6HzHLG6pgbS9UaTqg6fMISfzy2WGKokjK205ENFDDmEljj54/LTfdXgqg2XfYU4A==} resolution: {integrity: sha512-98T+3BesL4w/N39q/rgs9q6HzHLG6pgbS9UaTqg6fMISfzy2WGKokjK205ENFDDmEljj54/LTfdXgqg2XfYU4A==}
engines: {node: '>=16.14.0', npm: '>=8.1.0'} engines: {node: '>=16.14.0', npm: '>=8.1.0'}
@@ -2575,6 +2606,9 @@ packages:
quickselect@2.0.0: quickselect@2.0.0:
resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==} resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==}
quickselect@3.0.0:
resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==}
quotemeta@0.0.0: quotemeta@0.0.0:
resolution: {integrity: sha512-1XGObUh7RN5b58vKuAsrlfqT+Rc4vmw8N4pP9gFCq1GFlTdV0Ex/D2Ro1Drvrqj++HPi3ig0Np17XPslELeMRA==} resolution: {integrity: sha512-1XGObUh7RN5b58vKuAsrlfqT+Rc4vmw8N4pP9gFCq1GFlTdV0Ex/D2Ro1Drvrqj++HPi3ig0Np17XPslELeMRA==}
@@ -2748,6 +2782,10 @@ packages:
resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
hasBin: true hasBin: true
serialize-to-js@3.1.2:
resolution: {integrity: sha512-owllqNuDDEimQat7EPG0tH7JjO090xKNzUtYz6X+Sk2BXDnOCilDdNLwjWeFywG9xkJul1ULvtUQa9O4pUaY0w==}
engines: {node: '>=4.0.0'}
set-function-length@1.2.2: set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -2907,6 +2945,9 @@ packages:
tinyqueue@2.0.3: tinyqueue@2.0.3:
resolution: {integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==} resolution: {integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==}
tinyqueue@3.0.0:
resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==}
to-fast-properties@2.0.0: to-fast-properties@2.0.0:
resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -2936,6 +2977,9 @@ packages:
turf-jsts@1.2.3: turf-jsts@1.2.3:
resolution: {integrity: sha512-Ja03QIJlPuHt4IQ2FfGex4F4JAr8m3jpaHbFbQrgwr7s7L6U8ocrHiF3J1+wf9jzhGKxvDeaCAnGDot8OjGFyA==} resolution: {integrity: sha512-Ja03QIJlPuHt4IQ2FfGex4F4JAr8m3jpaHbFbQrgwr7s7L6U8ocrHiF3J1+wf9jzhGKxvDeaCAnGDot8OjGFyA==}
tweakpane@4.0.4:
resolution: {integrity: sha512-RkWD54zDlEbnN01wQPk0ANHGbdCvlJx/E8A1QxhTfCbX+ROWos1Ws2MnhOm39aUGMOh+36TjUwpDmLfmwTr1Fg==}
type-fest@2.19.0: type-fest@2.19.0:
resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==}
engines: {node: '>=12.20'} engines: {node: '>=12.20'}
@@ -3306,7 +3350,7 @@ snapshots:
'@biomejs/cli-win32-x64@1.8.2': '@biomejs/cli-win32-x64@1.8.2':
optional: true optional: true
'@buf/meshtastic_protobufs.bufbuild_es@1.10.0-20240820152623-fac6975bbc78.1(@bufbuild/protobuf@1.10.0)': '@buf/meshtastic_protobufs.bufbuild_es@1.10.0-20240906232734-3da561588c55.1(@bufbuild/protobuf@1.10.0)':
dependencies: dependencies:
'@bufbuild/protobuf': 1.10.0 '@bufbuild/protobuf': 1.10.0
@@ -3445,6 +3489,8 @@ snapshots:
'@mapbox/jsonlint-lines-primitives@2.0.2': {} '@mapbox/jsonlint-lines-primitives@2.0.2': {}
'@mapbox/mapbox-gl-supported@3.0.0': {}
'@mapbox/point-geometry@0.1.0': {} '@mapbox/point-geometry@0.1.0': {}
'@mapbox/tiny-sdf@2.0.6': {} '@mapbox/tiny-sdf@2.0.6': {}
@@ -3485,6 +3531,12 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- buffer - buffer
'@noble/curves@1.5.0':
dependencies:
'@noble/hashes': 1.4.0
'@noble/hashes@1.4.0': {}
'@nodelib/fs.scandir@2.1.5': '@nodelib/fs.scandir@2.1.5':
dependencies: dependencies:
'@nodelib/fs.stat': 2.0.5 '@nodelib/fs.stat': 2.0.5
@@ -5221,6 +5273,8 @@ snapshots:
escape-string-regexp: 1.0.5 escape-string-regexp: 1.0.5
supports-color: 5.5.0 supports-color: 5.5.0
cheap-ruler@4.0.0: {}
chokidar@3.6.0: chokidar@3.6.0:
dependencies: dependencies:
anymatch: 3.1.3 anymatch: 3.1.3
@@ -5308,6 +5362,8 @@ snapshots:
dependencies: dependencies:
type-fest: 2.19.0 type-fest: 2.19.0
csscolorparser@1.0.3: {}
cssesc@3.0.0: {} cssesc@3.0.0: {}
csstype@3.1.3: {} csstype@3.1.3: {}
@@ -5387,6 +5443,8 @@ snapshots:
earcut@2.2.4: {} earcut@2.2.4: {}
earcut@3.0.0: {}
eastasianwidth@0.2.0: {} eastasianwidth@0.2.0: {}
electron-to-chromium@1.4.812: {} electron-to-chromium@1.4.812: {}
@@ -5395,8 +5453,6 @@ snapshots:
emoji-regex@9.2.2: {} emoji-regex@9.2.2: {}
empty-npm-package@1.0.0: {}
end-of-stream@1.4.4: end-of-stream@1.4.4:
dependencies: dependencies:
once: 1.4.0 once: 1.4.0
@@ -5470,6 +5526,8 @@ snapshots:
dependencies: dependencies:
reusify: 1.0.4 reusify: 1.0.4
fflate@0.8.2: {}
fill-range@7.1.1: fill-range@7.1.1:
dependencies: dependencies:
to-regex-range: 5.0.1 to-regex-range: 5.0.1
@@ -5512,6 +5570,8 @@ snapshots:
geojson-vt@3.2.1: {} geojson-vt@3.2.1: {}
geojson-vt@4.0.2: {}
get-caller-file@2.0.5: {} get-caller-file@2.0.5: {}
get-intrinsic@1.2.4: get-intrinsic@1.2.4:
@@ -5559,6 +5619,8 @@ snapshots:
dependencies: dependencies:
get-intrinsic: 1.2.4 get-intrinsic: 1.2.4
grid-index@1.1.0: {}
gzipper@7.2.0: gzipper@7.2.0:
dependencies: dependencies:
'@gfx/zopfli': 1.0.15 '@gfx/zopfli': 1.0.15
@@ -5754,6 +5816,36 @@ snapshots:
dependencies: dependencies:
react: 18.3.1 react: 18.3.1
mapbox-gl@3.6.0:
dependencies:
'@mapbox/jsonlint-lines-primitives': 2.0.2
'@mapbox/mapbox-gl-supported': 3.0.0
'@mapbox/point-geometry': 0.1.0
'@mapbox/tiny-sdf': 2.0.6
'@mapbox/unitbezier': 0.0.1
'@mapbox/vector-tile': 1.3.1
'@mapbox/whoots-js': 3.1.0
'@types/geojson': 7946.0.14
'@types/mapbox__vector-tile': 1.3.4
cheap-ruler: 4.0.0
csscolorparser: 1.0.3
earcut: 3.0.0
fflate: 0.8.2
geojson-vt: 4.0.2
gl-matrix: 3.4.3
grid-index: 1.1.0
kdbush: 4.0.2
murmurhash-js: 1.0.0
pbf: 3.2.1
potpack: 2.0.0
quickselect: 3.0.0
rw: 1.3.3
serialize-to-js: 3.1.2
supercluster: 8.0.1
tinyqueue: 3.0.0
tweakpane: 4.0.4
vt-pbf: 3.1.3
maplibre-gl@4.1.2: maplibre-gl@4.1.2:
dependencies: dependencies:
'@mapbox/geojson-rewind': 0.5.2 '@mapbox/geojson-rewind': 0.5.2
@@ -5963,6 +6055,8 @@ snapshots:
quickselect@2.0.0: {} quickselect@2.0.0: {}
quickselect@3.0.0: {}
quotemeta@0.0.0: {} quotemeta@0.0.0: {}
rbush@2.0.2: rbush@2.0.2:
@@ -5985,14 +6079,14 @@ snapshots:
react-is@16.13.1: {} react-is@16.13.1: {}
react-map-gl@7.1.7(empty-npm-package@1.0.0)(maplibre-gl@4.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): react-map-gl@7.1.7(mapbox-gl@3.6.0)(maplibre-gl@4.1.2)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies: dependencies:
'@maplibre/maplibre-gl-style-spec': 19.3.3 '@maplibre/maplibre-gl-style-spec': 19.3.3
'@types/mapbox-gl': 3.1.0 '@types/mapbox-gl': 3.1.0
react: 18.3.1 react: 18.3.1
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
optionalDependencies: optionalDependencies:
mapbox-gl: empty-npm-package@1.0.0 mapbox-gl: 3.6.0
maplibre-gl: 4.1.2 maplibre-gl: 4.1.2
react-qrcode-logo@2.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): react-qrcode-logo@2.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
@@ -6153,6 +6247,8 @@ snapshots:
semver@6.3.1: {} semver@6.3.1: {}
serialize-to-js@3.1.2: {}
set-function-length@1.2.2: set-function-length@1.2.2:
dependencies: dependencies:
define-data-property: 1.1.4 define-data-property: 1.1.4
@@ -6358,6 +6454,8 @@ snapshots:
tinyqueue@2.0.3: {} tinyqueue@2.0.3: {}
tinyqueue@3.0.0: {}
to-fast-properties@2.0.0: {} to-fast-properties@2.0.0: {}
to-regex-range@5.0.1: to-regex-range@5.0.1:
@@ -6380,6 +6478,8 @@ snapshots:
turf-jsts@1.2.3: {} turf-jsts@1.2.3: {}
tweakpane@4.0.4: {}
type-fest@2.19.0: {} type-fest@2.19.0: {}
typescript@5.5.2: {} typescript@5.5.2: {}
+6 -1
View File
@@ -5,6 +5,7 @@ import { DeviceSelector } from "@components/DeviceSelector.js";
import { DialogManager } from "@components/Dialog/DialogManager.js"; import { DialogManager } from "@components/Dialog/DialogManager.js";
import { NewDeviceDialog } from "@components/Dialog/NewDeviceDialog.js"; import { NewDeviceDialog } from "@components/Dialog/NewDeviceDialog.js";
import { Toaster } from "@components/Toaster.js"; import { Toaster } from "@components/Toaster.js";
import Footer from "@components/UI/Footer.js";
import { ThemeController } from "@components/generic/ThemeController.js"; import { ThemeController } from "@components/generic/ThemeController.js";
import { useAppStore } from "@core/stores/appStore.js"; import { useAppStore } from "@core/stores/appStore.js";
import { useDeviceStore } from "@core/stores/deviceStore.js"; import { useDeviceStore } from "@core/stores/deviceStore.js";
@@ -40,7 +41,11 @@ export const App = (): JSX.Element => {
<PageRouter /> <PageRouter />
</div> </div>
) : ( ) : (
<Dashboard /> <>
<Dashboard />
<div className="flex flex-grow" />
<Footer />
</>
)} )}
</div> </div>
</div> </div>
@@ -0,0 +1,39 @@
import { Button } from "@components/UI/Button.js";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.js";
export interface PkiRegenerateDialogProps {
open: boolean;
onOpenChange: () => void;
onSubmit: () => void;
}
export const PkiRegenerateDialog = ({
open,
onOpenChange,
onSubmit,
}: PkiRegenerateDialogProps): JSX.Element => {
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent>
<DialogHeader>
<DialogTitle>Regenerate Key pair?</DialogTitle>
<DialogDescription>
Are you sure you want to regenerate key pair?
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button variant="destructive" onClick={() => onSubmit()}>
Regenerate
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
};
+7 -3
View File
@@ -4,11 +4,14 @@ import type {
} from "@components/Form/DynamicForm.js"; } from "@components/Form/DynamicForm.js";
import { Input } from "@components/UI/Input.js"; import { Input } from "@components/UI/Input.js";
import type { LucideIcon } from "lucide-react"; import type { LucideIcon } from "lucide-react";
import type { ChangeEventHandler } from "react";
import { Controller, type FieldValues } from "react-hook-form"; import { Controller, type FieldValues } from "react-hook-form";
export interface InputFieldProps<T> extends BaseFormBuilderProps<T> { export interface InputFieldProps<T> extends BaseFormBuilderProps<T> {
type: "text" | "number" | "password"; type: "text" | "number" | "password";
inputChange?: ChangeEventHandler;
properties?: { properties?: {
value?: string;
prefix?: string; prefix?: string;
suffix?: string; suffix?: string;
step?: number; step?: number;
@@ -33,13 +36,14 @@ export function GenericInput<T extends FieldValues>({
type={field.type} type={field.type}
step={field.properties?.step} step={field.properties?.step}
value={field.type === "number" ? Number.parseFloat(value) : value} value={field.type === "number" ? Number.parseFloat(value) : value}
onChange={(e) => onChange={(e) => {
if (field.inputChange) field.inputChange(e);
onChange( onChange(
field.type === "number" field.type === "number"
? Number.parseFloat(e.target.value) ? Number.parseFloat(e.target.value)
: e.target.value, : e.target.value,
) );
} }}
{...field.properties} {...field.properties}
{...rest} {...rest}
disabled={disabled} disabled={disabled}
@@ -9,6 +9,7 @@ import { Controller, type FieldValues } from "react-hook-form";
export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> { export interface PasswordGeneratorProps<T> extends BaseFormBuilderProps<T> {
type: "passwordGenerator"; type: "passwordGenerator";
hide?: boolean; hide?: boolean;
bits?: { text: string; value: string; key: string }[];
devicePSKBitCount: number; devicePSKBitCount: number;
inputChange: ChangeEventHandler; inputChange: ChangeEventHandler;
selectChange: (event: string) => void; selectChange: (event: string) => void;
@@ -28,6 +29,7 @@ export function PasswordGenerator<T extends FieldValues>({
<Generator <Generator
hide={field.hide} hide={field.hide}
devicePSKBitCount={field.devicePSKBitCount} devicePSKBitCount={field.devicePSKBitCount}
bits={field.bits}
inputChange={field.inputChange} inputChange={field.inputChange}
selectChange={field.selectChange} selectChange={field.selectChange}
buttonClick={field.buttonClick} buttonClick={field.buttonClick}
@@ -56,6 +56,13 @@ export const LoRa = (): JSX.Element => {
label: "Ignore MQTT", label: "Ignore MQTT",
description: "Don't forward MQTT messages over the mesh", description: "Don't forward MQTT messages over the mesh",
}, },
{
type: "toggle",
name: "configOkToMqtt",
label: "OK to MQTT",
description:
"When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT",
},
], ],
}, },
{ {
+161 -164
View File
@@ -1,9 +1,13 @@
import { PkiRegenerateDialog } from "@app/components/Dialog/PkiRegenerateDialog";
import { DynamicForm } from "@app/components/Form/DynamicForm.js"; import { DynamicForm } from "@app/components/Form/DynamicForm.js";
import {
getX25519PrivateKey,
getX25519PublicKey,
} from "@app/core/utils/x25519";
import type { SecurityValidation } from "@app/validation/config/security.js"; import type { SecurityValidation } from "@app/validation/config/security.js";
import { useDevice } from "@core/stores/deviceStore.js"; import { useDevice } from "@core/stores/deviceStore.js";
import { Protobuf } from "@meshtastic/js"; import { Protobuf } from "@meshtastic/js";
import { fromByteArray, toByteArray } from "base64-js"; import { fromByteArray, toByteArray } from "base64-js";
import cryptoRandomString from "crypto-random-string";
import { Eye, EyeOff } from "lucide-react"; import { Eye, EyeOff } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
@@ -15,7 +19,7 @@ export const Security = (): JSX.Element => {
); );
const [privateKeyVisible, setPrivateKeyVisible] = useState<boolean>(false); const [privateKeyVisible, setPrivateKeyVisible] = useState<boolean>(false);
const [privateKeyBitCount, setPrivateKeyBitCount] = useState<number>( const [privateKeyBitCount, setPrivateKeyBitCount] = useState<number>(
config.security?.privateKey.length ?? 16, config.security?.privateKey.length ?? 32,
); );
const [privateKeyValidationText, setPrivateKeyValidationText] = const [privateKeyValidationText, setPrivateKeyValidationText] =
useState<string>(); useState<string>();
@@ -23,14 +27,11 @@ export const Security = (): JSX.Element => {
fromByteArray(config.security?.publicKey ?? new Uint8Array(0)), fromByteArray(config.security?.publicKey ?? new Uint8Array(0)),
); );
const [adminKey, setAdminKey] = useState<string>( const [adminKey, setAdminKey] = useState<string>(
fromByteArray(config.security?.adminKey ?? new Uint8Array(0)), fromByteArray(config.security?.adminKey[0] ?? new Uint8Array(0)),
);
const [adminKeyVisible, setAdminKeyVisible] = useState<boolean>(false);
const [adminKeyBitCount, setAdminKeyBitCount] = useState<number>(
config.security?.adminKey.length ?? 16,
); );
const [adminKeyValidationText, setAdminKeyValidationText] = const [adminKeyValidationText, setAdminKeyValidationText] =
useState<string>(); useState<string>();
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const onSubmit = (data: SecurityValidation) => { const onSubmit = (data: SecurityValidation) => {
if (privateKeyValidationText || adminKeyValidationText) return; if (privateKeyValidationText || adminKeyValidationText) return;
@@ -41,7 +42,7 @@ export const Security = (): JSX.Element => {
case: "security", case: "security",
value: { value: {
...data, ...data,
adminKey: toByteArray(adminKey), adminKey: [toByteArray(adminKey)],
privateKey: toByteArray(privateKey), privateKey: toByteArray(privateKey),
publicKey: toByteArray(publicKey), publicKey: toByteArray(publicKey),
}, },
@@ -50,191 +51,187 @@ export const Security = (): JSX.Element => {
); );
}; };
const clickEvent = ( const validateKey = (
setKey: (value: React.SetStateAction<string>) => void,
bitCount: number,
setValidationText: (
value: React.SetStateAction<string | undefined>,
) => void,
) => {
setKey(
btoa(
cryptoRandomString({
length: bitCount ?? 0,
type: "alphanumeric",
}),
),
);
setValidationText(undefined);
};
const validatePass = (
input: string, input: string,
count: number, count: number,
setValidationText: ( setValidationText: (
value: React.SetStateAction<string | undefined>, value: React.SetStateAction<string | undefined>,
) => void, ) => void,
) => { ) => {
if (input.length % 4 !== 0 || toByteArray(input).length !== count) { try {
if (input.length % 4 !== 0 || toByteArray(input).length !== count) {
setValidationText(`Please enter a valid ${count * 8} bit PSK.`);
} else {
setValidationText(undefined);
}
} catch (e) {
console.error(e);
setValidationText(`Please enter a valid ${count * 8} bit PSK.`); setValidationText(`Please enter a valid ${count * 8} bit PSK.`);
} else {
setValidationText(undefined);
} }
}; };
const privateKeyClickEvent = () => {
setDialogOpen(true);
};
const pkiRegenerate = () => {
const privateKey = getX25519PrivateKey();
const publicKey = getX25519PublicKey(privateKey);
setPrivateKey(fromByteArray(privateKey));
setPublicKey(fromByteArray(publicKey));
validateKey(
fromByteArray(privateKey),
privateKeyBitCount,
setPrivateKeyValidationText,
);
setDialogOpen(false);
};
const privateKeyInputChangeEvent = ( const privateKeyInputChangeEvent = (
e: React.ChangeEvent<HTMLInputElement>, e: React.ChangeEvent<HTMLInputElement>,
) => { ) => {
const psk = e.currentTarget?.value; const privateKeyB64String = e.target.value;
setPrivateKey(psk); setPrivateKey(privateKeyB64String);
validatePass(psk, privateKeyBitCount, setPrivateKeyValidationText); validateKey(
privateKeyB64String,
privateKeyBitCount,
setPrivateKeyValidationText,
);
const publicKey = getX25519PublicKey(toByteArray(privateKeyB64String));
setPublicKey(fromByteArray(publicKey));
}; };
const adminKeyInputChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => { const adminKeyInputChangeEvent = (e: React.ChangeEvent<HTMLInputElement>) => {
const psk = e.currentTarget?.value; const psk = e.currentTarget?.value;
setAdminKey(psk); setAdminKey(psk);
validatePass(psk, privateKeyBitCount, setAdminKeyValidationText); validateKey(psk, privateKeyBitCount, setAdminKeyValidationText);
}; };
const privateKeySelectChangeEvent = (e: string) => { const privateKeySelectChangeEvent = (e: string) => {
const count = Number.parseInt(e); const count = Number.parseInt(e);
setPrivateKeyBitCount(count); setPrivateKeyBitCount(count);
validatePass(privateKey, count, setPrivateKeyValidationText); validateKey(privateKey, count, setPrivateKeyValidationText);
};
const adminKeySelectChangeEvent = (e: string) => {
const count = Number.parseInt(e);
setAdminKeyBitCount(count);
validatePass(privateKey, count, setAdminKeyValidationText);
}; };
return ( return (
<DynamicForm<SecurityValidation> <>
onSubmit={onSubmit} <DynamicForm<SecurityValidation>
defaultValues={{ onSubmit={onSubmit}
...config.security, submitType="onChange"
...{ defaultValues={{
adminKey: adminKey, ...config.security,
privateKey: privateKey, ...{
publicKey: publicKey, adminKey: adminKey,
}, privateKey: privateKey,
}} publicKey: publicKey,
fieldGroups={[ adminChannelEnabled: config.security?.adminChannelEnabled ?? false,
{ isManaged: config.security?.isManaged ?? false,
label: "Security Settings", debugLogApiEnabled: config.security?.debugLogApiEnabled ?? false,
description: "Settings for the Security configuration", serialEnabled: config.security?.serialEnabled ?? false,
fields: [ },
{ }}
type: "passwordGenerator", fieldGroups={[
name: "privateKey", {
label: "Private Key", label: "Security Settings",
description: "Used to create a shared key with a remote device", description: "Settings for the Security configuration",
validationText: privateKeyValidationText, fields: [
devicePSKBitCount: privateKeyBitCount, {
inputChange: privateKeyInputChangeEvent, type: "passwordGenerator",
selectChange: privateKeySelectChangeEvent, name: "privateKey",
hide: !privateKeyVisible, label: "Private Key",
buttonClick: () => description: "Used to create a shared key with a remote device",
clickEvent( bits: [{ text: "256 bit", value: "32", key: "bit256" }],
setPrivateKey, validationText: privateKeyValidationText,
privateKeyBitCount, devicePSKBitCount: privateKeyBitCount,
setPrivateKeyValidationText, inputChange: privateKeyInputChangeEvent,
), selectChange: privateKeySelectChangeEvent,
disabledBy: [ hide: !privateKeyVisible,
{ buttonClick: privateKeyClickEvent,
fieldName: "adminChannelEnabled", properties: {
invert: true, value: privateKey,
}, action: {
], icon: privateKeyVisible ? EyeOff : Eye,
properties: { onClick: () => setPrivateKeyVisible(!privateKeyVisible),
value: privateKey, },
action: {
icon: privateKeyVisible ? EyeOff : Eye,
onClick: () => setPrivateKeyVisible(!privateKeyVisible),
}, },
}, },
}, {
{ type: "text",
type: "text", name: "publicKey",
name: "publicKey", label: "Public Key",
label: "Public Key", disabled: true,
disabled: true, description:
description: "Sent out to other nodes on the mesh to allow them to compute a shared secret key",
"Sent out to other nodes on the mesh to allow them to compute a shared secret key", properties: {
}, value: publicKey,
],
},
{
label: "Admin Settings",
description: "Settings for Admin ",
fields: [
{
type: "toggle",
name: "adminChannelEnabled",
label: "Allow Legacy Admin",
description:
"Allow incoming device control over the insecure legacy admin channel",
},
{
type: "toggle",
name: "isManaged",
label: "Managed",
description:
'If true, device is considered to be "managed" by a mesh administrator via admin messages',
},
{
type: "passwordGenerator",
name: "adminKey",
label: "Admin Key",
description:
"The public key authorized to send admin messages to this node",
validationText: adminKeyValidationText,
devicePSKBitCount: adminKeyBitCount,
inputChange: adminKeyInputChangeEvent,
selectChange: adminKeySelectChangeEvent,
hide: !adminKeyVisible,
buttonClick: () =>
clickEvent(
setAdminKey,
adminKeyBitCount,
setAdminKeyValidationText,
),
disabledBy: [{ fieldName: "adminChannelEnabled" }],
properties: {
value: adminKey,
action: {
icon: adminKeyVisible ? EyeOff : Eye,
onClick: () => setAdminKeyVisible(!adminKeyVisible),
}, },
}, },
}, ],
], },
}, {
{ label: "Admin Settings",
label: "Logging Settings", description: "Settings for Admin",
description: "Settings for Logging", fields: [
fields: [ {
{ type: "toggle",
type: "toggle", name: "adminChannelEnabled",
name: "bluetoothLoggingEnabled", label: "Allow Legacy Admin",
label: "Allow Bluetooth Logging", description:
description: "Enables device (serial style logs) over Bluetooth", "Allow incoming device control over the insecure legacy admin channel",
}, },
{ {
type: "toggle", type: "toggle",
name: "debugLogApiEnabled", name: "isManaged",
label: "Enable Debug Log API", label: "Managed",
description: "Output live debug logging over serial", description:
}, 'If true, device is considered to be "managed" by a mesh administrator via admin messages',
{ },
type: "toggle", {
name: "serialEnabled", type: "text",
label: "Serial Output Enabled", name: "adminKey",
description: "Serial Console over the Stream API", label: "Admin Key",
}, description:
], "The public key authorized to send admin messages to this node",
}, validationText: adminKeyValidationText,
]} inputChange: adminKeyInputChangeEvent,
/> disabledBy: [
{ fieldName: "adminChannelEnabled", invert: true },
],
properties: {
value: adminKey,
},
},
],
},
{
label: "Logging Settings",
description: "Settings for Logging",
fields: [
{
type: "toggle",
name: "debugLogApiEnabled",
label: "Enable Debug Log API",
description:
"Output live debug logging over serial, view and export position-redacted device logs over Bluetooth",
},
{
type: "toggle",
name: "serialEnabled",
label: "Serial Output Enabled",
description: "Serial Console over the Stream API",
},
],
},
]}
/>
<PkiRegenerateDialog
open={dialogOpen}
onOpenChange={() => setDialogOpen(false)}
onSubmit={() => pkiRegenerate()}
/>
</>
); );
}; };
@@ -68,7 +68,7 @@ export const ChannelChat = ({
)} )}
</div> </div>
</div> </div>
<div className="p-3"> <div className="pl-3 pr-3 pt-3 pb-1">
<MessageInput to={to} channel={channel} /> <MessageInput to={to} channel={channel} />
</div> </div>
</div> </div>
@@ -87,7 +87,7 @@ export const Telemetry = (): JSX.Element => {
description: "How often to send Power data over the mesh", description: "How often to send Power data over the mesh",
}, },
{ {
type: "text", type: "toggle",
name: "powerScreenEnabled", name: "powerScreenEnabled",
label: "Power Screen Enabled", label: "Power Screen Enabled",
description: "Enable the Power Telemetry Screen", description: "Enable the Power Telemetry Screen",
+36 -31
View File
@@ -1,5 +1,6 @@
import { cn } from "@app/core/utils/cn.js"; import { cn } from "@app/core/utils/cn.js";
import { AlignLeftIcon, type LucideIcon } from "lucide-react"; import { AlignLeftIcon, type LucideIcon } from "lucide-react";
import Footer from "./UI/Footer";
export interface PageLayoutProps { export interface PageLayoutProps {
label: string; label: string;
@@ -7,6 +8,7 @@ export interface PageLayoutProps {
children: React.ReactNode; children: React.ReactNode;
actions?: { actions?: {
icon: LucideIcon; icon: LucideIcon;
iconClasses?: string;
onClick: () => void; onClick: () => void;
}[]; }[];
} }
@@ -18,40 +20,43 @@ export const PageLayout = ({
children, children,
}: PageLayoutProps): JSX.Element => { }: PageLayoutProps): JSX.Element => {
return ( return (
<div className="relative flex h-full w-full flex-col"> <>
<div className="flex h-14 shrink-0 border-b-[0.5px] border-slate-300 dark:border-slate-700 md:h-16 md:px-4"> <div className="relative flex h-full w-full flex-col">
<button <div className="flex h-14 shrink-0 border-b-[0.5px] border-slate-300 dark:border-slate-700 md:h-16 md:px-4">
type="button" <button
className="pl-4 transition-all hover:text-accent md:hidden" type="button"
> className="pl-4 transition-all hover:text-accent md:hidden"
<AlignLeftIcon /> >
</button> <AlignLeftIcon />
<div className="flex flex-1 items-center justify-between px-4 md:px-0"> </button>
<div className="flex w-full items-center"> <div className="flex flex-1 items-center justify-between px-4 md:px-0">
<span className="w-full text-lg font-medium">{label}</span> <div className="flex w-full items-center">
<div className="flex justify-end space-x-4"> <span className="w-full text-lg font-medium">{label}</span>
{actions?.map((action, index) => ( <div className="flex justify-end space-x-4">
<button {actions?.map((action, index) => (
key={action.icon.name} <button
type="button" key={action.icon.name}
className="transition-all hover:text-accent" type="button"
onClick={action.onClick} className="transition-all hover:text-accent"
> onClick={action.onClick}
<action.icon /> >
</button> <action.icon className={action.iconClasses} />
))} </button>
))}
</div>
</div> </div>
</div> </div>
</div> </div>
<div
className={cn(
"flex h-full w-full flex-col overflow-y-auto",
!noPadding && "pl-3 pr-3 ",
)}
>
{children}
<Footer />
</div>
</div> </div>
<div </>
className={cn(
"flex h-full w-full flex-col overflow-y-auto",
!noPadding && "p-3",
)}
>
{children}
</div>
</div>
); );
}; };
+37
View File
@@ -0,0 +1,37 @@
import React from "react";
export interface FooterProps extends React.HTMLAttributes<HTMLElement> {}
const Footer = React.forwardRef<HTMLElement, FooterProps>(
({ className, ...props }, ref) => {
return (
<footer
className={`flex flex- justify-center p-2 ${className}`}
style={{
backgroundColor: "var(--backgroundPrimary)",
color: "var(--textPrimary)",
}}
>
<p>
<a
href="https://vercel.com/?utm_source=meshtastic&utm_campaign=oss"
className="hover:underline"
style={{ color: "var(--link)" }}
>
Powered by ▲ Vercel
</a>{" "}
| Meshtastic® is a registered trademark of Meshtastic LLC. |{" "}
<a
href="https://meshtastic.org/docs/legal"
className="hover:underline"
style={{ color: "var(--link)" }}
>
Legal Information
</a>
</p>
</footer>
);
},
);
export default Footer;
+29 -9
View File
@@ -17,6 +17,7 @@ export interface GeneratorProps extends React.BaseHTMLAttributes<HTMLElement> {
value: string; value: string;
variant: "default" | "invalid"; variant: "default" | "invalid";
buttonText?: string; buttonText?: string;
bits?: { text: string; value: string; key: string }[];
selectChange: (event: string) => void; selectChange: (event: string) => void;
inputChange: (event: React.ChangeEvent<HTMLInputElement>) => void; inputChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
buttonClick: React.MouseEventHandler<HTMLButtonElement>; buttonClick: React.MouseEventHandler<HTMLButtonElement>;
@@ -35,6 +36,11 @@ const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
variant, variant,
value, value,
buttonText, buttonText,
bits = [
{ text: "256 bit", value: "32", key: "bit256" },
{ text: "128 bit", value: "16", key: "bit128" },
{ text: "8 bit", value: "1", key: "bit8" },
],
selectChange, selectChange,
inputChange, inputChange,
buttonClick, buttonClick,
@@ -44,6 +50,21 @@ const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
}, },
ref, ref,
) => { ) => {
const inputRef = React.useRef<HTMLInputElement>(null);
// Invokes onChange event on the input element when the value changes from the parent component
React.useEffect(() => {
if (!inputRef.current) return;
const setValue = Object.getOwnPropertyDescriptor(
HTMLInputElement.prototype,
"value",
)?.set;
if (!setValue) return;
inputRef.current.value = "";
setValue.call(inputRef.current, value);
inputRef.current.dispatchEvent(new Event("input", { bubbles: true }));
}, [value]);
return ( return (
<> <>
<Input <Input
@@ -54,30 +75,29 @@ const Generator = React.forwardRef<HTMLInputElement, GeneratorProps>(
onChange={inputChange} onChange={inputChange}
action={action} action={action}
disabled={disabled} disabled={disabled}
ref={inputRef}
/> />
<Select <Select
value={devicePSKBitCount?.toString()} value={devicePSKBitCount?.toString()}
onValueChange={(e) => selectChange(e)} onValueChange={(e) => selectChange(e)}
disabled={disabled}
> >
<SelectTrigger className="!max-w-max"> <SelectTrigger className="!max-w-max">
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem key="bit256" value="32"> {bits.map(({ text, value, key }) => (
256 bit <SelectItem key={key} value={value}>
</SelectItem> {text}
<SelectItem key="bit128" value="16"> </SelectItem>
128 bit ))}
</SelectItem>
<SelectItem key="bit8" value="1">
8 bit
</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
<Button <Button
type="button" type="button"
variant="success" variant="success"
onClick={buttonClick} onClick={buttonClick}
disabled={disabled}
{...props} {...props}
> >
{buttonText} {buttonText}
+2 -1
View File
@@ -31,7 +31,7 @@ export interface InputProps
} }
const Input = React.forwardRef<HTMLInputElement, InputProps>( const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, variant, prefix, suffix, action, ...props }, ref) => { ({ className, value, variant, prefix, suffix, action, ...props }, ref) => {
return ( return (
<div className="relative w-full"> <div className="relative w-full">
{prefix && ( {prefix && (
@@ -45,6 +45,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
className, className,
inputVariants({ variant }), inputVariants({ variant }),
)} )}
value={value}
ref={ref} ref={ref}
{...props} {...props}
/> />
+1 -1
View File
@@ -12,7 +12,7 @@ const TabsList = React.forwardRef<
<TabsPrimitive.List <TabsPrimitive.List
ref={ref} ref={ref}
className={cn( className={cn(
"inline-flex flex-wrap items-center rounded-md bg-slate-100 p-1 dark:bg-slate-800", "inline-flex flex-wrap items-center rounded-md bg-slate-100 p-1 mt-2 dark:bg-slate-800",
className, className,
)} )}
{...props} {...props}
+15
View File
@@ -0,0 +1,15 @@
import { x25519 } from "@noble/curves/ed25519";
export function getX25519PrivateKey(): Uint8Array {
const key = x25519.utils.randomPrivateKey();
key[0] &= 248;
key[31] &= 127;
key[31] |= 64;
return key;
}
export function getX25519PublicKey(privateKey: Uint8Array): Uint8Array {
return x25519.getPublicKey(privateKey);
}
+74 -72
View File
@@ -21,79 +21,81 @@ export const Dashboard = () => {
const devices = useMemo(() => getDevices(), [getDevices]); const devices = useMemo(() => getDevices(), [getDevices]);
return ( return (
<div className="flex flex-col gap-3 p-3"> <>
<div className="flex items-center justify-between"> <div className="flex flex-col gap-3 p-3">
<div className="space-y-1"> <div className="flex items-center justify-between">
<H3>Connected Devices</H3> <div className="space-y-1">
<Subtle>Manage, connect and disconnect devices</Subtle> <H3>Connected Devices</H3>
<Subtle>Manage, connect and disconnect devices</Subtle>
</div>
</div>
<Separator />
<div className="flex h-[450px] rounded-md border border-dashed border-slate-200 p-3 dark:border-slate-700">
{devices.length ? (
<ul className="grow divide-y divide-gray-200">
{devices.map((device) => {
return (
<li key={device.id}>
<div className="px-4 py-4 sm:px-6">
<div className="flex items-center justify-between">
<p className="truncate text-sm font-medium text-accent">
{device.nodes.get(device.hardware.myNodeNum)?.user
?.longName ?? "UNK"}
</p>
<div className="inline-flex w-24 justify-center gap-2 rounded-full bg-slate-100 py-1 text-xs font-semibold text-slate-900 transition-colors hover:bg-slate-700 hover:text-slate-50">
{device.connection?.connType === "ble" && (
<>
<BluetoothIcon size={16} />
BLE
</>
)}
{device.connection?.connType === "serial" && (
<>
<UsbIcon size={16} />
Serial
</>
)}
{device.connection?.connType === "http" && (
<>
<NetworkIcon size={16} />
Network
</>
)}
</div>
</div>
<div className="mt-2 sm:flex sm:justify-between">
<div className="flex gap-2 text-sm text-gray-500">
<UsersIcon
size={20}
className="text-gray-400"
aria-hidden="true"
/>
{device.nodes.size === 0 ? 0 : device.nodes.size - 1}
</div>
</div>
</div>
</li>
);
})}
</ul>
) : (
<div className="m-auto flex flex-col gap-3 text-center">
<ListPlusIcon size={48} className="mx-auto text-textSecondary" />
<H3>No Devices</H3>
<Subtle>Connect at least one device to get started</Subtle>
<Button
className="gap-2"
onClick={() => setConnectDialogOpen(true)}
>
<PlusIcon size={16} />
New Connection
</Button>
</div>
)}
</div> </div>
</div> </div>
</>
<Separator />
<div className="flex h-[450px] rounded-md border border-dashed border-slate-200 p-3 dark:border-slate-700">
{devices.length ? (
<ul className="grow divide-y divide-gray-200">
{devices.map((device) => {
return (
<li key={device.id}>
<div className="px-4 py-4 sm:px-6">
<div className="flex items-center justify-between">
<p className="truncate text-sm font-medium text-accent">
{device.nodes.get(device.hardware.myNodeNum)?.user
?.longName ?? "UNK"}
</p>
<div className="inline-flex w-24 justify-center gap-2 rounded-full bg-slate-100 py-1 text-xs font-semibold text-slate-900 transition-colors hover:bg-slate-700 hover:text-slate-50">
{device.connection?.connType === "ble" && (
<>
<BluetoothIcon size={16} />
BLE
</>
)}
{device.connection?.connType === "serial" && (
<>
<UsbIcon size={16} />
Serial
</>
)}
{device.connection?.connType === "http" && (
<>
<NetworkIcon size={16} />
Network
</>
)}
</div>
</div>
<div className="mt-2 sm:flex sm:justify-between">
<div className="flex gap-2 text-sm text-gray-500">
<UsersIcon
size={20}
className="text-gray-400"
aria-hidden="true"
/>
{device.nodes.size === 0 ? 0 : device.nodes.size - 1}
</div>
</div>
</div>
</li>
);
})}
</ul>
) : (
<div className="m-auto flex flex-col gap-3 text-center">
<ListPlusIcon size={48} className="mx-auto text-textSecondary" />
<H3>No Devices</H3>
<Subtle>Connect atleast one device to get started</Subtle>
<Button
className="gap-2"
onClick={() => setConnectDialogOpen(true)}
>
<PlusIcon size={16} />
New Connection
</Button>
</div>
)}
</div>
</div>
); );
}; };
+72 -53
View File
@@ -8,7 +8,7 @@ import { useDevice } from "@core/stores/deviceStore.js";
import { Hashicon } from "@emeraldpay/hashicon-react"; import { Hashicon } from "@emeraldpay/hashicon-react";
import { Protobuf, Types } from "@meshtastic/js"; import { Protobuf, Types } from "@meshtastic/js";
import { getChannelName } from "@pages/Channels.js"; import { getChannelName } from "@pages/Channels.js";
import { HashIcon, WaypointsIcon } from "lucide-react"; import { HashIcon, LockIcon, LockOpenIcon, WaypointsIcon } from "lucide-react";
import { useState } from "react"; import { useState } from "react";
export const MessagesPage = (): JSX.Element => { export const MessagesPage = (): JSX.Element => {
@@ -67,60 +67,79 @@ export const MessagesPage = (): JSX.Element => {
))} ))}
</SidebarSection> </SidebarSection>
</Sidebar> </Sidebar>
<PageLayout <div className="flex flex-col flex-grow">
label={`Messages: ${ <PageLayout
chatType === "broadcast" && currentChannel label={`Messages: ${
? getChannelName(currentChannel) chatType === "broadcast" && currentChannel
: chatType === "direct" && nodes.get(activeChat) ? getChannelName(currentChannel)
? nodes.get(activeChat)?.user?.longName ?? "Unknown" : chatType === "direct" && nodes.get(activeChat)
: "Loading..." ? nodes.get(activeChat)?.user?.longName ?? "Unknown"
}`} : "Loading..."
actions={ }`}
chatType === "direct" actions={
? [ chatType === "direct"
{ ? [
icon: WaypointsIcon, {
async onClick() { icon: nodes.get(activeChat)?.user?.publicKey.length
const targetNode = nodes.get(activeChat)?.num; ? LockIcon
if (targetNode === undefined) return; : LockOpenIcon,
toast({ iconClasses: nodes.get(activeChat)?.user?.publicKey.length
title: "Sending Traceroute, please wait...", ? "text-green-600"
}); : "text-yellow-300",
await connection?.traceRoute(targetNode).then(() => async onClick() {
const targetNode = nodes.get(activeChat)?.num;
if (targetNode === undefined) return;
toast({ toast({
title: "Traceroute sent.", title: nodes.get(activeChat)?.user?.publicKey.length
}), ? "Chat is using PKI encryption."
); : "Chat is using PSK encryption.",
});
},
}, },
}, {
] icon: WaypointsIcon,
: [] async onClick() {
} const targetNode = nodes.get(activeChat)?.num;
> if (targetNode === undefined) return;
{allChannels.map( toast({
(channel) => title: "Sending Traceroute, please wait...",
activeChat === channel.index && ( });
<ChannelChat await connection?.traceRoute(targetNode).then(() =>
key={channel.index} toast({
to="broadcast" title: "Traceroute sent.",
messages={messages.broadcast.get(channel.index)} }),
channel={channel.index} );
/> },
), },
)} ]
{filteredNodes.map( : []
(node) => }
activeChat === node.num && ( >
<ChannelChat {allChannels.map(
key={node.num} (channel) =>
to={activeChat} activeChat === channel.index && (
messages={messages.direct.get(node.num)} <ChannelChat
channel={Types.ChannelNumber.Primary} key={channel.index}
traceroutes={traceroutes.get(node.num)} to="broadcast"
/> messages={messages.broadcast.get(channel.index)}
), channel={channel.index}
)} />
</PageLayout> ),
)}
{filteredNodes.map(
(node) =>
activeChat === node.num && (
<ChannelChat
key={node.num}
to={activeChat}
messages={messages.direct.get(node.num)}
channel={Types.ChannelNumber.Primary}
traceroutes={traceroutes.get(node.num)}
/>
),
)}
</PageLayout>
</div>
</> </>
); );
}; };
+79 -67
View File
@@ -1,3 +1,4 @@
import Footer from "@app/components/UI/Footer";
import { useAppStore } from "@app/core/stores/appStore"; import { useAppStore } from "@app/core/stores/appStore";
import { Sidebar } from "@components/Sidebar.js"; import { Sidebar } from "@components/Sidebar.js";
import { Button } from "@components/UI/Button.js"; import { Button } from "@components/UI/Button.js";
@@ -7,7 +8,7 @@ import { TimeAgo } from "@components/generic/Table/tmp/TimeAgo.js";
import { useDevice } from "@core/stores/deviceStore.js"; import { useDevice } from "@core/stores/deviceStore.js";
import { Hashicon } from "@emeraldpay/hashicon-react"; import { Hashicon } from "@emeraldpay/hashicon-react";
import { Protobuf } from "@meshtastic/js"; import { Protobuf } from "@meshtastic/js";
import { TrashIcon } from "lucide-react"; import { LockIcon, LockOpenIcon, TrashIcon } from "lucide-react";
import { Fragment } from "react"; import { Fragment } from "react";
import { base16 } from "rfc4648"; import { base16 } from "rfc4648";
@@ -27,73 +28,84 @@ export const NodesPage = (): JSX.Element => {
return ( return (
<> <>
<Sidebar /> <Sidebar />
<div className="w-full overflow-y-auto"> <div className="flex flex-col w-full">
<Table <div className="overflow-y-auto h-full">
headings={[ <Table
{ title: "", type: "blank", sortable: false }, headings={[
{ title: "Name", type: "normal", sortable: true }, { title: "", type: "blank", sortable: false },
{ title: "Model", type: "normal", sortable: true }, { title: "Name", type: "normal", sortable: true },
{ title: "MAC Address", type: "normal", sortable: true }, { title: "Model", type: "normal", sortable: true },
{ title: "Last Heard", type: "normal", sortable: true }, { title: "MAC Address", type: "normal", sortable: true },
{ title: "SNR", type: "normal", sortable: true }, { title: "Last Heard", type: "normal", sortable: true },
{ title: "Connection", type: "normal", sortable: true }, { title: "SNR", type: "normal", sortable: true },
{ title: "Remove", type: "normal", sortable: false }, { title: "Encryption", type: "normal", sortable: false },
]} { title: "Connection", type: "normal", sortable: true },
rows={filteredNodes.map((node) => [ { title: "Remove", type: "normal", sortable: false },
<Hashicon key="icon" size={24} value={node.num.toString()} />, ]}
<h1 key="header"> rows={filteredNodes.map((node) => [
{node.user?.longName ?? <Hashicon key="icon" size={24} value={node.num.toString()} />,
(node.user?.macaddr <h1 key="header">
? `Meshtastic ${base16 {node.user?.longName ??
.stringify(node.user?.macaddr.subarray(4, 6) ?? []) (node.user?.macaddr
.toLowerCase()}` ? `Meshtastic ${base16
: `UNK: ${node.num}`)} .stringify(node.user?.macaddr.subarray(4, 6) ?? [])
</h1>, .toLowerCase()}`
: `UNK: ${node.num}`)}
</h1>,
<Mono key="model"> <Mono key="model">
{Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]} {Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]}
</Mono>, </Mono>,
<Mono key="addr"> <Mono key="addr">
{base16 {base16
.stringify(node.user?.macaddr ?? []) .stringify(node.user?.macaddr ?? [])
.match(/.{1,2}/g) .match(/.{1,2}/g)
?.join(":") ?? "UNK"} ?.join(":") ?? "UNK"}
</Mono>, </Mono>,
<Fragment key="lastHeard"> <Fragment key="lastHeard">
{node.lastHeard === 0 ? ( {node.lastHeard === 0 ? (
<p>Never</p> <p>Never</p>
) : ( ) : (
<TimeAgo timestamp={node.lastHeard * 1000} /> <TimeAgo timestamp={node.lastHeard * 1000} />
)} )}
</Fragment>, </Fragment>,
<Mono key="snr"> <Mono key="snr">
{node.snr}db/ {node.snr}db/
{Math.min(Math.max((node.snr + 10) * 5, 0), 100)}%/ {Math.min(Math.max((node.snr + 10) * 5, 0), 100)}%/
{(node.snr + 10) * 5}raw {(node.snr + 10) * 5}raw
</Mono>, </Mono>,
<Mono key="hops"> <Mono key="pki">
{node.lastHeard !== 0 {node.user?.publicKey && node.user?.publicKey.length > 0 ? (
? node.viaMqtt === false && node.hopsAway === 0 <LockIcon className="text-green-600" />
? "Direct" ) : (
: `${node.hopsAway.toString()} ${ <LockOpenIcon className="text-yellow-300" />
node.hopsAway > 1 ? "hops" : "hop" )}
} away` </Mono>,
: "-"} <Mono key="hops">
{node.viaMqtt === true ? ", via MQTT" : ""} {node.lastHeard !== 0
</Mono>, ? node.viaMqtt === false && node.hopsAway === 0
<Button ? "Direct"
key="remove" : `${node.hopsAway.toString()} ${
variant="destructive" node.hopsAway > 1 ? "hops" : "hop"
onClick={() => { } away`
setNodeNumToBeRemoved(node.num); : "-"}
setDialogOpen("nodeRemoval", true); {node.viaMqtt === true ? ", via MQTT" : ""}
}} </Mono>,
> <Button
<TrashIcon /> key="remove"
Remove variant="destructive"
</Button>, onClick={() => {
])} setNodeNumToBeRemoved(node.num);
/> setDialogOpen("nodeRemoval", true);
}}
>
<TrashIcon />
Remove
</Button>,
])}
/>
</div>
<Footer />
</div> </div>
</> </>
); );
+3
View File
@@ -60,4 +60,7 @@ export class LoRaValidation
@IsBoolean() @IsBoolean()
ignoreMqtt: boolean; ignoreMqtt: boolean;
@IsBoolean()
configOkToMqtt: boolean;
} }