diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 2a67cb6b..08dd6bd2 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -35,9 +35,8 @@ jobs: restore-keys: | ${{ runner.os }}-pnpm- - # - name: Run tests - # working-directory: packages/web - # run: deno task test + - name: Run tests + run: pnpm run test - name: Install Dependencies working-directory: packages/web diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index dcc7d332..98c1bb76 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -44,6 +44,9 @@ jobs: - name: Check formatter run: pnpm run check + - name: Run tests + run: pnpm run test + - name: Build Package working-directory: packages/web run: pnpm run build diff --git a/.github/workflows/release-packages.yml b/.github/workflows/release-packages.yml index 50dcf4ab..17f920ec 100644 --- a/.github/workflows/release-packages.yml +++ b/.github/workflows/release-packages.yml @@ -61,26 +61,31 @@ jobs: - name: Publish packages to npm and JSR run: | for dir in packages/*; do - echo "Processing $dir" - - cd $dir - - # Build and publish to npm if package.json exists - if [ -f "package.json" ]; then - echo "Building and publishing $dir to npm..." - pnpm run build:npm - pnpm run publish:npm || echo "npm publish failed for $dir" - fi - - pnpm run prepare:jsr - - # Publish to JSR if jsr.json exists - if [ -f "jsr.json" ]; then - echo "Publishing $dir to jsr..." - deno publish || echo "JSR publish failed for $dir" + if [ "$dir" != "packages/web" ]; then + echo "Processing $dir" + + cd $dir + + # Build and publish to npm if package.json exists + if [ -f "package.json" ]; then + echo "Building and publishing $dir to npm..." + pnpm install + pnpm run build:npm + pnpm run publish:npm || echo "npm publish failed for $dir" + fi + + pnpm run prepare:jsr + + # Publish to JSR if jsr.json exists + if [ -f "jsr.json" ]; then + echo "Publishing $dir to jsr..." + deno publish || echo "JSR publish failed for $dir" + fi + + cd - > /dev/null + else + echo "Skipping $dir" fi - - cd - > /dev/null done \ No newline at end of file diff --git a/.github/workflows/release-web.yml b/.github/workflows/release-web.yml index b33a124d..9fe4d576 100644 --- a/.github/workflows/release-web.yml +++ b/.github/workflows/release-web.yml @@ -13,31 +13,36 @@ jobs: steps: - name: Checkout Code uses: actions/checkout@v4 + + # --- Setup Node.js and pnpm --- + - name: Setup Node.js + uses: actions/setup-node@v4 with: - fetch-depth: 0 + node-version: 22 - - name: Setup Bun - uses: oven-sh/setup-bun@v2 + - name: Setup pnpm + uses: pnpm/action-setup@v4 with: - bun-version: latest + version: latest - - name: Cache Bun Dependencies + # --- Cache pnpm Dependencies --- + - name: Cache pnpm Dependencies uses: actions/cache@v4 with: path: | - ~/.bun/install/cache + ~/.pnpm-store packages/web/node_modules - key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }} + key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} restore-keys: | - ${{ runner.os }}-bun- + ${{ runner.os }}-pnpm- - - name: Run Web App Tests + - name: Install dependencies working-directory: packages/web - run: bun run test + run: pnpm install - name: Create Web App Release Archive working-directory: packages/web - run: bun run package + run: pnpm run package - name: Upload Web App Archive uses: actions/upload-artifact@v4 @@ -59,7 +64,7 @@ jobs: uses: redhat-actions/buildah-build@v2 with: containerfiles: | - ./infra/Containerfile + ./packages/web/infra/Containerfile image: ghcr.io/${{ github.repository }} tags: latest, ${{ github.event.release.tag_name }} oci: true diff --git a/package.json b/package.json index 4d7b56c8..492a2d07 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,8 @@ "check:fix": "biome check --write", "build:all": "pnpm run --filter '*' build", "clean:all": "pnpm run --filter '*' clean", - "publish:packages": "pnpm run --filter 'packages/transport-* packages/core' build" + "publish:packages": "pnpm run --filter 'packages/transport-* packages/core' build", + "test": "vitest" }, "dependencies": { "@bufbuild/protobuf": "^2.6.1", @@ -37,6 +38,7 @@ "@types/node": "^22.16.4", "biome": "^0.3.3", "tsdown": "^0.13.4", - "typescript": "^5.8.3" + "typescript": "^5.8.3", + "vitest": "^3.2.4" } } diff --git a/packages/core/package.json b/packages/core/package.json index 2f68d362..77383ea9 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -10,7 +10,7 @@ "types": "./dist/mod.d.mts", "license": "GPL-3.0-only", "tsdown": { - "entry": ["mod.ts"], + "entry": "mod.ts", "dts": true, "format": ["esm"], "splitting": false, diff --git a/packages/core/src/utils/eventSystem.ts b/packages/core/src/utils/eventSystem.ts index f5f6afbf..81b24d34 100644 --- a/packages/core/src/utils/eventSystem.ts +++ b/packages/core/src/utils/eventSystem.ts @@ -330,6 +330,15 @@ export class EventSystem { PacketMetadata > = new SimpleEventDispatcher>(); + /** + * Fires when a new MeshPacket message containing a ClientNotification packet has been + * received from device + * + * @event onClientNotificationPacket + */ + public readonly onClientNotificationPacket: SimpleEventDispatcher = + new SimpleEventDispatcher(); + /** * Fires when the devices connection or configuration status changes * diff --git a/packages/core/src/utils/transform/decodePacket.ts b/packages/core/src/utils/transform/decodePacket.ts index f1f2eb09..94f0e54d 100644 --- a/packages/core/src/utils/transform/decodePacket.ts +++ b/packages/core/src/utils/transform/decodePacket.ts @@ -209,6 +209,18 @@ export const decodePacket = (device: MeshDevice) => break; } + case "clientNotification": { + device.log.trace( + Types.Emitter[Types.Emitter.HandleFromRadio], + `📣 Received ClientNotification: ${decodedMessage.payloadVariant.value.message}`, + ); + + device.events.onClientNotificationPacket.dispatch( + decodedMessage.payloadVariant.value, + ); + break; + } + default: { device.log.warn( Types.Emitter[Types.Emitter.HandleFromRadio], diff --git a/packages/transport-deno/package.json b/packages/transport-deno/package.json index dd92c9b5..531deb36 100644 --- a/packages/transport-deno/package.json +++ b/packages/transport-deno/package.json @@ -10,6 +10,13 @@ "types": "./dist/mod.d.mts", "files": ["dist/*", "mod.ts", "README.md", "../../LICENSE"], "license": "GPL-3.0-only", + "tsdown": { + "entry": "mod.ts", + "dts": true, + "format": ["esm"], + "splitting": false, + "clean": true + }, "scripts": { "preinstall": "npx only-allow pnpm", "prepack": "cp ../../LICENSE ./LICENSE", @@ -18,5 +25,8 @@ "publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public", "prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr", "publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check" + }, + "dependencies": { + "@meshtastic/core": "workspace:*" } } diff --git a/packages/transport-http/package.json b/packages/transport-http/package.json index 77b74b69..64ba5a47 100644 --- a/packages/transport-http/package.json +++ b/packages/transport-http/package.json @@ -8,7 +8,7 @@ "types": "./dist/mod.d.mts", "license": "GPL-3.0-only", "tsdown": { - "entry": ["mod.ts"], + "entry": "mod.ts", "dts": true, "format": ["esm"], "splitting": false, @@ -28,5 +28,8 @@ "publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public", "prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr", "publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check" + }, + "dependencies": { + "@meshtastic/core": "workspace:*" } } diff --git a/packages/transport-node/package.json b/packages/transport-node/package.json index d150990d..c92e562b 100644 --- a/packages/transport-node/package.json +++ b/packages/transport-node/package.json @@ -11,7 +11,7 @@ "license": "GPL-3.0-only", "tsdown": { - "entry": ["mod.ts"], + "entry": "mod.ts", "dts": true, "format": ["esm"], "splitting": false, @@ -31,5 +31,8 @@ "publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public", "prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr", "publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check" + }, + "dependencies": { + "@meshtastic/core": "workspace:*" } } diff --git a/packages/transport-web-bluetooth/package.json b/packages/transport-web-bluetooth/package.json index c0105a49..f1562983 100644 --- a/packages/transport-web-bluetooth/package.json +++ b/packages/transport-web-bluetooth/package.json @@ -1,17 +1,17 @@ { "name": "@meshtastic/transport-web-bluetooth", - "version": "0.1.4", - "description": "A transport layer for Meshtastic applications using Web Bluetooth.", - "exports": { - ".": "./mod.ts" - }, + "version": "0.1.4", + "description": "A transport layer for Meshtastic applications using Web Bluetooth.", + "exports": { + ".": "./mod.ts" + }, "main": "./dist/mod.mjs", "module": "./dist/mod.mjs", "types": "./dist/mod.d.mts", "files": ["dist/*", "mod.ts", "README.md", "../../LICENSE"], "license": "GPL-3.0-only", "tsdown": { - "entry": ["mod.ts"], + "entry": "mod.ts", "dts": true, "format": ["esm"], "splitting": false, @@ -26,8 +26,8 @@ "prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr", "publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check" }, - "dependencies": { - "@types/web-bluetooth": "npm:@types/web-bluetooth@^0.0.20" - } - + "dependencies": { + "@types/web-bluetooth": "npm:@types/web-bluetooth@^0.0.20", + "@meshtastic/core": "workspace:*" + } } \ No newline at end of file diff --git a/packages/transport-web-serial/package.json b/packages/transport-web-serial/package.json index c1260053..90535ba6 100644 --- a/packages/transport-web-serial/package.json +++ b/packages/transport-web-serial/package.json @@ -11,7 +11,7 @@ "files": ["dist/*", "mod.ts", "README.md", "../../LICENSE"], "license": "GPL-3.0-only", "tsdown": { - "entry": ["mod.ts"], + "entry": "mod.ts", "dts": true, "format": ["esm"], "splitting": false, @@ -27,6 +27,7 @@ "publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check" }, "dependencies": { - "@types/w3c-web-serial": "npm:@types/w3c-web-serial@^1.0.7" + "@types/w3c-web-serial": "npm:@types/w3c-web-serial@^1.0.7", + "@meshtastic/core": "workspace:*" } } \ No newline at end of file diff --git a/packages/web/infra/Containerfile b/packages/web/infra/Containerfile index 16c29639..1a982c22 100644 --- a/packages/web/infra/Containerfile +++ b/packages/web/infra/Containerfile @@ -1,12 +1,12 @@ FROM nginx:1.27-alpine RUN rm -r /usr/share/nginx/html \ - && mkdir -p /usr/share/nginx/html \ - && mkdir -p /etc/nginx/conf.d + && mkdir -p /usr/share/nginx/html \ + && mkdir -p /etc/nginx/conf.d WORKDIR /usr/share/nginx/html -ADD dist . +ADD ./dist . COPY ./infra/default.conf /etc/nginx/conf.d/default.conf diff --git a/packages/web/package.json b/packages/web/package.json index 49880828..88e4b6ed 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -29,9 +29,9 @@ "@bufbuild/protobuf": "^2.6.0", "@hookform/resolvers": "^5.1.1", "@meshtastic/core": "workspace:*", - "@meshtastic/transport-http": "npm:@jsr/meshtastic__transport-http", - "@meshtastic/transport-web-bluetooth": "npm:@jsr/meshtastic__transport-web-bluetooth", - "@meshtastic/transport-web-serial": "npm:@jsr/meshtastic__transport-web-serial", + "@meshtastic/transport-http": "workspace:*", + "@meshtastic/transport-web-bluetooth": "workspace:*", + "@meshtastic/transport-web-serial": "workspace:*", "@noble/curves": "^1.9.2", "@radix-ui/react-accordion": "^1.2.11", "@radix-ui/react-checkbox": "^1.3.2", diff --git a/packages/web/public/i18n/locales/en/common.json b/packages/web/public/i18n/locales/en/common.json index d6cd2a17..e0e95049 100644 --- a/packages/web/public/i18n/locales/en/common.json +++ b/packages/web/public/i18n/locales/en/common.json @@ -107,5 +107,7 @@ "managed": "At least one admin key is requred if the node is managed.", "key": "Key is required." } - } + }, + "yes": "Yes", + "no": "No" } diff --git a/packages/web/public/i18n/locales/en/dialog.json b/packages/web/public/i18n/locales/en/dialog.json index c343b369..73a869d5 100644 --- a/packages/web/public/i18n/locales/en/dialog.json +++ b/packages/web/public/i18n/locales/en/dialog.json @@ -93,7 +93,7 @@ "deviceMetrics": "Device Metrics:", "hardware": "Hardware: ", "lastHeard": "Last Heard: ", - "nodeHexPrefix": "Node Hex: !", + "nodeHexPrefix": "Node Hex: ", "nodeNumber": "Node Number: ", "position": "Position:", "role": "Role: ", @@ -102,7 +102,12 @@ "title": "Node Details for {{identifier}}", "ignoreNode": "Ignore node", "removeNode": "Remove node", - "unignoreNode": "Unignore node" + "unignoreNode": "Unignore node", + "security": "Security:", + "publicKey": "Public Key: ", + "messageable": "Messageable: ", + "KeyManuallyVerifiedTrue": "Public Key has been manually verified", + "KeyManuallyVerifiedFalse": "Public Key is not manually verified" }, "pkiBackup": { "loseKeysWarning": "If you lose your keys, you will need to reset your device.", @@ -179,5 +184,10 @@ "confirmUnderstanding": "Yes, I know what I'm doing", "title": "Are you sure?", "description": "Enabling Managed Mode blocks client applications (including the web client) from writing configurations to a radio. Once enabled, radio configurations can only be changed through Remote Admin messages. This setting is not required for remote node administration." + }, + "clientNotification": { + "title": "Client Notification", + "TraceRoute can only be sent once every 30 seconds": "TraceRoute can only be sent once every 30 seconds", + "Compromised keys were detected and regenerated.": "Compromised keys were detected and regenerated." } } diff --git a/packages/web/src/components/Dialog/ClientNotificationDialog/ClientNotificationDialog.tsx b/packages/web/src/components/Dialog/ClientNotificationDialog/ClientNotificationDialog.tsx new file mode 100644 index 00000000..92d68531 --- /dev/null +++ b/packages/web/src/components/Dialog/ClientNotificationDialog/ClientNotificationDialog.tsx @@ -0,0 +1,72 @@ +import { + Dialog, + DialogClose, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@components/UI/Dialog.tsx"; +import { useDevice } from "@core/stores/deviceStore.ts"; +import { useTranslation } from "react-i18next"; + +export interface ClientNotificationDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; +} + +export const ClientNotificationDialog = ({ + open, + onOpenChange, +}: ClientNotificationDialogProps) => { + const { t } = useTranslation("dialog"); + const { getClientNotification, removeClientNotification } = useDevice(); + + const localOnOpenChange = (open: boolean) => { + removeClientNotification(0); + if (!getClientNotification(0)) { + onOpenChange(open); + } + }; + + const dialogContent = (() => { + if (!getClientNotification(0)) { + return; + } + + switch (getClientNotification(0)?.payloadVariant.case) { + // TODO: Add KeyVerification logic + /*case "keyVerificationNumberInform": + return <>; + case "keyVerificationNumberRequest": + return <>; + case "keyVerificationFinal": + return <>; + case "duplicatedPublicKey": + return <>; + case "lowEntropyKey": + return <>;*/ + + default: + return ( + + {t("clientNotification.title")} + + {t([ + `clientNotification.${getClientNotification(0)?.message}`, + getClientNotification(0)?.message ?? "", + ])} + + + ); + } + })(); + + return ( + + + + {dialogContent} + + + ); +}; diff --git a/packages/web/src/components/Dialog/DialogManager.tsx b/packages/web/src/components/Dialog/DialogManager.tsx index 9c6734f8..48af8290 100644 --- a/packages/web/src/components/Dialog/DialogManager.tsx +++ b/packages/web/src/components/Dialog/DialogManager.tsx @@ -1,3 +1,4 @@ +import { ClientNotificationDialog } from "@components/Dialog/ClientNotificationDialog/ClientNotificationDialog.tsx"; import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx"; import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.tsx"; import { ImportDialog } from "@components/Dialog/ImportDialog.tsx"; @@ -84,6 +85,12 @@ export const DialogManager = () => { setDialogOpen("deleteMessages", open); }} /> + { + setDialogOpen("clientNotification", open); + }} + /> ); }; diff --git a/packages/web/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx b/packages/web/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx index ec4de469..a6b9104e 100644 --- a/packages/web/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx +++ b/packages/web/src/components/Dialog/NodeDetailsDialog/NodeDetailsDialog.tsx @@ -32,6 +32,7 @@ import { cn } from "@core/utils/cn.ts"; import { Protobuf } from "@meshtastic/core"; import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { useNavigate } from "@tanstack/react-router"; +import { fromByteArray } from "base64-js"; import { BellIcon, BellOffIcon, @@ -166,7 +167,8 @@ export const NodeDetailsDialog = ({ key: "batteryLevel", label: t("nodeDetails.batteryLevel"), value: node.deviceMetrics?.batteryLevel, - format: (val: number) => `${val.toFixed(2)}%`, + format: (val: number) => + val === 101 ? t("batteryStatus.pluggedIn") : `${val.toFixed(2)}%`, }, { key: "voltage", @@ -176,6 +178,9 @@ export const NodeDetailsDialog = ({ }, ]; + const sectionClassName = + "text-slate-900 dark:text-slate-100 bg-slate-100 dark:bg-slate-800 p-4 rounded-lg mt-3"; + return ( @@ -191,7 +196,7 @@ export const NodeDetailsDialog = ({ -
+
-
+
+

+ {t("nodeDetails.security")} +

+ + + + + + + + + + + +
{t("nodeDetails.publicKey")} +
+                          {node.user?.publicKey &&
+                          node.user?.publicKey.length > 0
+                            ? fromByteArray(node.user.publicKey)
+                            : t("unknown.longName")}
+                        
+
+ {node.isKeyManuallyVerified + ? t("nodeDetails.KeyManuallyVerifiedTrue") + : t("nodeDetails.KeyManuallyVerifiedFalse")} +
+
+ +

{t("nodeDetails.position")}

{node.position ? ( - <> - {node.position.latitudeI && node.position.longitudeI && ( -

- {t("locationResponse.coordinates")} - - {node.position.latitudeI / 1e7},{" "} - {node.position.longitudeI / 1e7} - -

- )} - {node.position.altitude && ( -

- {t("locationResponse.altitude")} - {node.position.altitude} - {t("unit.meter.one")} -

- )} - + + + {node.position.latitudeI && node.position.longitudeI && ( + + + + + )} + {node.position.altitude && ( + + + + + )} + +
{t("locationResponse.coordinates")} + + {node.position.latitudeI / 1e7},{" "} + {node.position.longitudeI / 1e7} + +
{t("locationResponse.altitude")} + {node.position.altitude} + {t("unit.meter.suffix")} +
) : ( -

{t("unknown.shortName")}

+

{t("unknown.longName")}

)}