Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
40c2bfa535 | ||
|
|
a10023fad6 | ||
|
|
0a1afa988e | ||
|
|
97b884f119 | ||
|
|
eb9b7159d4 | ||
|
|
65a53388bb | ||
|
|
2ca3eb5685 | ||
|
|
a4c817cd30 | ||
|
|
d5b03c09db | ||
|
|
0b9ebade38 | ||
|
|
ee1758a548 | ||
|
|
a2a45ac898 | ||
|
|
59d172765d | ||
|
|
d453ff809a | ||
|
|
ed0a99dbd9 | ||
|
|
32f31cb502 | ||
|
|
176d554ef9 | ||
|
|
2735c37fad | ||
|
|
f04ec36faf | ||
|
|
28f0ca4337 | ||
|
|
1dbf0b07b6 | ||
|
|
27ed4e58bd | ||
|
|
a7f56c0bd5 |
@@ -16,16 +16,18 @@ jobs:
|
|||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
cache: pnpm
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Setup Deno
|
- name: Setup Deno
|
||||||
uses: denoland/setup-deno@v2
|
uses: denoland/setup-deno@v2
|
||||||
with:
|
with:
|
||||||
@@ -59,7 +61,7 @@ jobs:
|
|||||||
|
|
||||||
if [[ -f "$pkg_dir/package.json" ]] && [[ "$pkg_dir" != "packages/web" ]]; then
|
if [[ -f "$pkg_dir/package.json" ]] && [[ "$pkg_dir" != "packages/web" ]]; then
|
||||||
echo "🔧 Building with pnpm: $pkg_dir"
|
echo "🔧 Building with pnpm: $pkg_dir"
|
||||||
(cd "$pkg_dir" && pnpm install && pnpm run build:npm)
|
(cd "$pkg_dir" && pnpm install --frozen-lockfile && pnpm run build:npm)
|
||||||
else
|
else
|
||||||
echo "⚠️ Skipping $pkg_dir (web package or no package.json)"
|
echo "⚠️ Skipping $pkg_dir (web package or no package.json)"
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -1,92 +1,100 @@
|
|||||||
name: "Nightly Release"
|
name: Nightly Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
schedule:
|
schedule:
|
||||||
- cron: "0 5 * * *" # Run every day at 5am UTC
|
- cron: "0 5 * * *" # 05:00 UTC daily
|
||||||
|
workflow_dispatch: {} # allow manual runs too
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: read
|
||||||
packages: write
|
packages: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY_IMAGE: ghcr.io/${{ github.repository }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
nightly-build-and-push:
|
nightly-build-and-push:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
|
||||||
- name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Setup Node.js
|
steps:
|
||||||
uses: actions/setup-node@v4
|
- name: Checkout code
|
||||||
with:
|
uses: actions/checkout@v4
|
||||||
node-version: 22
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
|
|
||||||
- name: Cache pnpm dependencies
|
- name: Setup Node.js
|
||||||
uses: actions/cache@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
node-version: 22
|
||||||
~/.pnpm-store
|
cache: pnpm
|
||||||
packages/web/node_modules
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('packages/web/pnpm-lock.yaml') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pnpm-
|
|
||||||
|
|
||||||
# - name: Run tests
|
- name: Install dependencies (root)
|
||||||
# working-directory: packages/web
|
run: pnpm install --frozen-lockfile
|
||||||
# run: deno task test
|
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Run tests
|
||||||
working-directory: packages/web
|
run: pnpm run test
|
||||||
run: pnpm install
|
|
||||||
|
|
||||||
- name: Build Package
|
- name: Build web package
|
||||||
working-directory: packages/web
|
working-directory: packages/web
|
||||||
run: pnpm run build
|
run: pnpm run build
|
||||||
|
|
||||||
- name: Package Output
|
- name: Package output
|
||||||
working-directory: packages/web
|
working-directory: packages/web
|
||||||
run: pnpm run package
|
run: pnpm run package
|
||||||
|
|
||||||
- name: Archive compressed build
|
- name: Upload compressed build (artifact)
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: build
|
name: web-build-nightly
|
||||||
path: packages/web/dist/build.tar
|
path: packages/web/dist/build.tar
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
- name: Get latest release version
|
- name: Determine nightly tag
|
||||||
id: get_release
|
id: meta
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
LATEST_TAG=$(curl -sL \
|
set -euo pipefail
|
||||||
-H "Accept: application/vnd.github+json" \
|
DATE="$(date -u +%Y%m%d)"
|
||||||
-H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
SHORTSHA="$(git rev-parse --short=12 HEAD)"
|
||||||
https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r ".tag_name")
|
# Try to use latest release tag if it exists; fallback to package version; else date
|
||||||
# Fallback to a default if no release is found
|
LATEST_TAG="$(gh release view --json tagName --jq .tagName 2>/dev/null || true)"
|
||||||
if [ -z "$LATEST_TAG" ]; then
|
if [ -z "$LATEST_TAG" ] && [ -f packages/web/package.json ]; then
|
||||||
LATEST_TAG="2.6.0"
|
LATEST_TAG="v$(jq -r .version packages/web/package.json)"
|
||||||
fi
|
fi
|
||||||
echo "tag=${LATEST_TAG}" >> $GITHUB_OUTPUT
|
if [ -n "$LATEST_TAG" ] && [ "$LATEST_TAG" != "vnull" ]; then
|
||||||
|
TAG="nightly-${LATEST_TAG}-${SHORTSHA}"
|
||||||
|
else
|
||||||
|
TAG="nightly-${DATE}-${SHORTSHA}"
|
||||||
|
fi
|
||||||
|
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "tags=nightly, $TAG" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Resolved nightly tags: nightly, $TAG"
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
- name: Buildah Build
|
- name: Build Container Image (multi-arch)
|
||||||
id: build-container
|
id: build-container
|
||||||
uses: redhat-actions/buildah-build@v2
|
uses: redhat-actions/buildah-build@v2
|
||||||
with:
|
with:
|
||||||
containerfiles: |
|
containerfiles: |
|
||||||
./packages/web/infra/Containerfile
|
./packages/web/infra/Containerfile
|
||||||
image: ${{ github.event.repository.full_name }}
|
image: ${{ env.REGISTRY_IMAGE }}
|
||||||
tags: nightly-${{ steps.get_release.outputs.tag }}-${{ github.sha }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
|
||||||
|
|
||||||
oci: true
|
oci: true
|
||||||
platforms: linux/amd64, linux/arm64
|
platforms: linux/amd64, linux/arm64
|
||||||
|
labels: |
|
||||||
|
org.opencontainers.image.source=${{ github.repository }}
|
||||||
|
org.opencontainers.image.revision=${{ github.sha }}
|
||||||
|
org.opencontainers.image.created=${{ github.run_id }}
|
||||||
|
|
||||||
- name: Push To Registry
|
- name: Push To GHCR
|
||||||
id: push-to-registry
|
id: push-to-registry
|
||||||
uses: redhat-actions/push-to-registry@v2
|
uses: redhat-actions/push-to-registry@v2
|
||||||
with:
|
with:
|
||||||
@@ -96,6 +104,5 @@ jobs:
|
|||||||
username: ${{ github.actor }}
|
username: ${{ github.actor }}
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Print image url
|
- name: Print image URL
|
||||||
run: echo "Image pushed to ${{ steps.push-to-registry.outputs.registry-paths }}"
|
run: echo "Image pushed to ${{ steps.push-to-registry.outputs.registry-paths }}"
|
||||||
|
|
||||||
|
|||||||
+27
-24
@@ -1,42 +1,43 @@
|
|||||||
name: Pull Request CI
|
name: Pull Request CI
|
||||||
|
|
||||||
on: pull_request
|
on:
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened, ready_for_review]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: read
|
||||||
packages: write
|
|
||||||
|
concurrency:
|
||||||
|
group: pr-${{ github.event.pull_request.number }}-ci
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
CI: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-and-package:
|
build-and-package:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
# Skip for draft PRs; remove this line if you want to run on drafts too
|
||||||
|
if: ${{ github.event.pull_request.draft == false }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Setup Node.js
|
|
||||||
uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: 22
|
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
|
|
||||||
- name: Install Dependencies
|
- name: Setup Node.js
|
||||||
# Commands will run from 'packages/web'
|
uses: actions/setup-node@v4
|
||||||
working-directory: packages/web
|
|
||||||
run: pnpm install
|
|
||||||
|
|
||||||
- name: Cache pnpm dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
with:
|
||||||
path: |
|
node-version: 22
|
||||||
~/.pnpm-store
|
cache: pnpm
|
||||||
packages/web/node_modules
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('packages/web/pnpm-lock.yaml') }}
|
|
||||||
restore-keys: |
|
- name: Install dependencies
|
||||||
${{ runner.os }}-pnpm-
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: pnpm run lint
|
run: pnpm run lint
|
||||||
@@ -44,6 +45,8 @@ jobs:
|
|||||||
- name: Check formatter
|
- name: Check formatter
|
||||||
run: pnpm run check
|
run: pnpm run check
|
||||||
|
|
||||||
- name: Build Package
|
- name: Run tests
|
||||||
working-directory: packages/web
|
run: pnpm run test
|
||||||
run: pnpm run build
|
|
||||||
|
- name: Build web package
|
||||||
|
run: pnpm --filter "./packages/web" run build
|
||||||
|
|||||||
@@ -11,76 +11,103 @@ on:
|
|||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
id-token: write # <-- required for JSR OIDC
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# --- Setup Node.js and pnpm ---
|
# Node + pnpm (with cache)
|
||||||
- name: Setup Node.js
|
- name: Setup Node
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 22
|
node-version: 22
|
||||||
|
cache: pnpm
|
||||||
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
|
|
||||||
- name: Setup pnpm
|
- name: Setup pnpm
|
||||||
uses: pnpm/action-setup@v4
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
|
|
||||||
# --- Setup Deno ---
|
- name: Configure npm auth
|
||||||
- name: Setup Deno
|
env:
|
||||||
uses: denoland/setup-deno@v2
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
with:
|
|
||||||
deno-version: v2.x
|
|
||||||
|
|
||||||
# --- Cache pnpm Dependencies ---
|
|
||||||
- name: Cache pnpm Dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: |
|
|
||||||
~/.pnpm-store
|
|
||||||
packages/web/node_modules
|
|
||||||
key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-pnpm-
|
|
||||||
|
|
||||||
# --- Cache Deno Dependencies ---
|
|
||||||
- name: Cache Deno Dependencies
|
|
||||||
uses: actions/cache@v4
|
|
||||||
with:
|
|
||||||
path: ~/.cache/deno
|
|
||||||
key: ${{ runner.os }}-deno-${{ hashFiles('**/deno.lock') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-deno-
|
|
||||||
|
|
||||||
- name: Configure pnpm registry
|
|
||||||
run: pnpm config set registry https://registry.npmjs.org/
|
|
||||||
|
|
||||||
- name: Configure pnpm auth
|
|
||||||
run: pnpm config set //registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}
|
|
||||||
|
|
||||||
- name: Publish packages to npm and JSR
|
|
||||||
run: |
|
run: |
|
||||||
for dir in packages/*; do
|
pnpm config set //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
|
||||||
echo "Processing $dir"
|
pnpm config set registry https://registry.npmjs.org/
|
||||||
|
|
||||||
cd $dir
|
- name: Install deps (root)
|
||||||
|
run: pnpm install
|
||||||
|
|
||||||
# Build and publish to npm if package.json exists
|
- name: Resolve package list
|
||||||
if [ -f "package.json" ]; then
|
id: pkgs
|
||||||
echo "Building and publishing $dir to npm..."
|
shell: bash
|
||||||
pnpm run build:npm
|
run: |
|
||||||
pnpm run publish:npm || echo "npm publish failed for $dir"
|
if [ "${{ github.event.inputs.packages }}" = "all" ] || [ -z "${{ github.event.inputs.packages }}" ]; then
|
||||||
|
mapfile -t TARGETS < <(ls -d packages/* | grep -v 'packages/web')
|
||||||
|
else
|
||||||
|
IFS=',' read -ra TARGETS <<< "${{ github.event.inputs.packages }}"
|
||||||
|
TARGETS=("${TARGETS[@]/#/packages/}")
|
||||||
|
fi
|
||||||
|
printf '%s\n' "${TARGETS[@]}" | paste -sd, - > targets.txt
|
||||||
|
echo "list=$(cat targets.txt)" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
|
- name: Build selected packages (tsdown)
|
||||||
|
run: |
|
||||||
|
IFS=',' read -ra TARGETS <<< "${{ steps.pkgs.outputs.list }}"
|
||||||
|
for dir in "${TARGETS[@]}"; do
|
||||||
|
echo "Building $dir"
|
||||||
|
pnpm --filter "./$dir" run build
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Sync jsr.json version from package.json
|
||||||
|
run: |
|
||||||
|
IFS=',' read -ra TARGETS <<< "${{ steps.pkgs.outputs.list }}"
|
||||||
|
for dir in "${TARGETS[@]}"; do
|
||||||
|
if [ -f "$dir/jsr.json" ] && [ -f "$dir/package.json" ]; then
|
||||||
|
PKG_VER=$(jq -r .version "$dir/package.json")
|
||||||
|
jq --arg v "$PKG_VER" '.version = $v' "$dir/jsr.json" > "$dir/jsr.json.tmp" && mv "$dir/jsr.json.tmp" "$dir/jsr.json"
|
||||||
|
echo "Updated $dir/jsr.json to version $PKG_VER"
|
||||||
fi
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
pnpm run prepare:jsr
|
- name: Publish to JSR (OIDC)
|
||||||
|
run: |
|
||||||
# Publish to JSR if jsr.json exists
|
set -euo pipefail
|
||||||
if [ -f "jsr.json" ]; then
|
IFS=',' read -ra TARGETS <<< "${{ steps.pkgs.outputs.list }}"
|
||||||
echo "Publishing $dir to jsr..."
|
for dir in "${TARGETS[@]}"; do
|
||||||
deno publish || echo "JSR publish failed for $dir"
|
if [ -f "$dir/jsr.json" ]; then
|
||||||
|
echo "Publishing $dir to JSR via OIDC…"
|
||||||
|
cd "$dir"
|
||||||
|
[ -d dist ] || pnpm run build
|
||||||
|
if ! npx --yes jsr publish 2>&1 | tee jsr_publish.log; then
|
||||||
|
echo "JSR publish failed for $dir. Error output:"
|
||||||
|
cat jsr_publish.log
|
||||||
|
fi
|
||||||
|
cd - >/dev/null
|
||||||
fi
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
cd - > /dev/null
|
- name: Replace exports entry in package.json
|
||||||
|
run: |
|
||||||
|
tmp=$(mktemp)
|
||||||
|
jq '.exports["."] = "./dist/mod.mjs"' package.json > "$tmp" \
|
||||||
|
&& mv "$tmp" package.json
|
||||||
|
|
||||||
|
- name: Publish to npm
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
IFS=',' read -ra TARGETS <<< "${{ steps.pkgs.outputs.list }}"
|
||||||
|
for dir in "${TARGETS[@]}"; do
|
||||||
|
if [ -f "$dir/package.json" ]; then
|
||||||
|
echo "Publishing $dir to npm…"
|
||||||
|
cd "$dir"
|
||||||
|
[ -d dist ] || pnpm run build
|
||||||
|
npm publish --access public || echo "npm publish failed for $dir"
|
||||||
|
cd - >/dev/null
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
|
|
||||||
@@ -3,9 +3,28 @@ name: Release Web
|
|||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [released, prereleased]
|
types: [released, prereleased]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
ref:
|
||||||
|
description: "Git ref (branch, tag, or SHA) to build"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
tag_name:
|
||||||
|
description: "Tag to use for artifacts/images (defaults to <sha>)"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
attach_to_release:
|
||||||
|
description: "Upload build.tar to an existing GitHub Release (requires tag_name)"
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY_IMAGE: ghcr.io/${{ github.repository }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release-web:
|
release-web:
|
||||||
@@ -14,54 +33,113 @@ jobs:
|
|||||||
- name: Checkout Code
|
- name: Checkout Code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
# For manual runs, allow building a chosen ref (branch/tag/SHA)
|
||||||
|
ref: ${{ inputs.ref != '' && inputs.ref || github.ref }}
|
||||||
|
|
||||||
- name: Setup Bun
|
- name: Determine tag & latest flag
|
||||||
uses: oven-sh/setup-bun@v2
|
id: meta
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
# Determine TAG
|
||||||
|
if [ "${{ github.event_name }}" = "release" ]; then
|
||||||
|
TAG="${{ github.event.release.tag_name }}"
|
||||||
|
# Push "latest" only for full releases (not prereleases)
|
||||||
|
if [ "${{ github.event.release.prerelease }}" = "true" ]; then
|
||||||
|
PUSH_LATEST="false"
|
||||||
|
else
|
||||||
|
PUSH_LATEST="true"
|
||||||
|
fi
|
||||||
|
elif [ -n "${{ inputs.tag_name }}" ]; then
|
||||||
|
TAG="${{ inputs.tag_name }}"
|
||||||
|
PUSH_LATEST="false"
|
||||||
|
else
|
||||||
|
SHA="$(git rev-parse --short=12 HEAD)"
|
||||||
|
TAG="adhoc-${SHA}"
|
||||||
|
PUSH_LATEST="false"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "push_latest=$PUSH_LATEST" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Resolved tag: $TAG (push_latest=$PUSH_LATEST)"
|
||||||
|
|
||||||
|
- name: Setup pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
bun-version: latest
|
version: latest
|
||||||
|
|
||||||
- name: Cache Bun Dependencies
|
- name: Setup Node.js
|
||||||
uses: actions/cache@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
path: |
|
node-version: 22
|
||||||
~/.bun/install/cache
|
cache: pnpm
|
||||||
packages/web/node_modules
|
cache-dependency-path: '**/pnpm-lock.yaml'
|
||||||
key: ${{ runner.os }}-bun-${{ hashFiles('**/bun.lockb') }}
|
|
||||||
restore-keys: |
|
|
||||||
${{ runner.os }}-bun-
|
|
||||||
|
|
||||||
- name: Run Web App Tests
|
- name: Install dependencies
|
||||||
|
run: pnpm install --frozen-lockfile
|
||||||
|
|
||||||
|
- name: Build web package
|
||||||
working-directory: packages/web
|
working-directory: packages/web
|
||||||
run: bun run test
|
run: pnpm run build
|
||||||
|
|
||||||
|
- name: Build web package
|
||||||
|
working-directory: packages/web
|
||||||
|
run: pnpm run build
|
||||||
|
|
||||||
- name: Create Web App Release Archive
|
- name: Create Web App Release Archive
|
||||||
working-directory: packages/web
|
working-directory: packages/web
|
||||||
run: bun run package
|
run: pnpm run package
|
||||||
|
|
||||||
- name: Upload Web App Archive
|
- name: Upload Web App Archive (artifact)
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: web-build
|
name: web-build-${{ steps.meta.outputs.tag }}
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
path: packages/web/dist/build.tar
|
path: packages/web/dist/build.tar
|
||||||
|
|
||||||
- name: Attach Web Archive to GitHub Release
|
- name: Attach Web Archive to GitHub Release
|
||||||
run: gh release upload ${{ github.event.release.tag_name }} packages/web/dist/build.tar
|
if: ${{ github.event_name == 'release' || inputs.attach_to_release == true }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ "${{ github.event_name }}" = "release" ]; then
|
||||||
|
TAG="${{ steps.meta.outputs.tag }}"
|
||||||
|
else
|
||||||
|
if [ -z "${{ inputs.tag_name }}" ]; then
|
||||||
|
echo "attach_to_release requested but no tag_name provided." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
TAG="${{ inputs.tag_name }}"
|
||||||
|
fi
|
||||||
|
gh release upload "$TAG" packages/web/dist/build.tar --clobber
|
||||||
|
|
||||||
- name: Set up QEMU
|
- name: Set up QEMU
|
||||||
uses: docker/setup-qemu-action@v3
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Compute image tags
|
||||||
|
id: tags
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
if [ "${{ steps.meta.outputs.push_latest }}" = "true" ]; then
|
||||||
|
echo "list=latest, ${{ steps.meta.outputs.tag }}" >> "$GITHUB_OUTPUT"
|
||||||
|
else
|
||||||
|
echo "list=${{ steps.meta.outputs.tag }}" >> "$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
TAGS="latest, ${{ steps.meta.outputs.tag }}"
|
||||||
|
else
|
||||||
|
TAGS="${{ steps.meta.outputs.tag }}"
|
||||||
|
fi
|
||||||
|
echo "list=$TAGS" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "Using image tags: $TAGS"
|
||||||
|
|
||||||
- name: Build Container Image
|
- name: Build Container Image
|
||||||
id: build-container
|
id: build-container
|
||||||
uses: redhat-actions/buildah-build@v2
|
uses: redhat-actions/buildah-build@v2
|
||||||
with:
|
with:
|
||||||
containerfiles: |
|
containerfiles: |
|
||||||
./infra/Containerfile
|
./packages/web/infra/Containerfile
|
||||||
image: ghcr.io/${{ github.repository }}
|
image: ${{ env.REGISTRY_IMAGE }}
|
||||||
tags: latest, ${{ github.event.release.tag_name }}
|
tags: ${{ steps.tags.outputs.list }}
|
||||||
oci: true
|
oci: true
|
||||||
platforms: linux/amd64, linux/arm64
|
platforms: linux/amd64, linux/arm64
|
||||||
|
|
||||||
|
|||||||
@@ -7,44 +7,67 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: update-stable-${{ github.run_id }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
update-stable-branch:
|
update-stable-branch:
|
||||||
name: Update Stable Branch from Main
|
name: Update stable from latest release source
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0 # need full history for reset/push
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Configure Git
|
- name: Configure Git author
|
||||||
run: |
|
run: |
|
||||||
git config user.name "GitHub Actions Bot"
|
git config user.name "github-actions[bot]"
|
||||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||||
|
|
||||||
- name: Fetch latest main and stable branches
|
- name: Determine source ref & SHA
|
||||||
|
id: meta
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
git fetch origin main:main
|
set -euo pipefail
|
||||||
git fetch origin stable:stable || echo "Stable branch not found remotely, will create."
|
SRC="${{ github.event.release.target_commitish }}"
|
||||||
|
if [ -z "$SRC" ] || ! git ls-remote --exit-code origin "refs/heads/$SRC" >/dev/null 2>&1; then
|
||||||
- name: Get latest main commit SHA
|
# Fallback to main if target_commitish is empty or not a branch
|
||||||
id: get_main_sha
|
SRC="main"
|
||||||
run: echo "MAIN_SHA=$(git rev-parse main)" >> $GITHUB_ENV
|
|
||||||
|
|
||||||
- name: Check out stable branch
|
|
||||||
run: |
|
|
||||||
if git show-ref --verify --quiet refs/heads/stable; then
|
|
||||||
git checkout stable
|
|
||||||
git pull origin stable # Sync with remote stable if it exists
|
|
||||||
else
|
|
||||||
echo "Creating local stable branch based on main HEAD."
|
|
||||||
git checkout -b stable ${{ env.MAIN_SHA }}
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Reset stable branch to latest main
|
echo "Using source branch: $SRC"
|
||||||
run: git reset --hard ${{ env.MAIN_SHA }}
|
git fetch origin "$SRC":"refs/remotes/origin/$SRC" --prune
|
||||||
|
SHA="$(git rev-parse "origin/$SRC")"
|
||||||
|
echo "sha=$SHA" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "src=$SRC" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Force push stable branch
|
- name: Prepare local stable branch
|
||||||
run: git push origin stable --force
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
# Ensure we have the remote stable ref if it exists
|
||||||
|
git fetch origin stable:refs/remotes/origin/stable || true
|
||||||
|
|
||||||
|
if git show-ref --verify --quiet refs/heads/stable; then
|
||||||
|
echo "Local stable exists."
|
||||||
|
elif git show-ref --verify --quiet refs/remotes/origin/stable; then
|
||||||
|
echo "Creating local stable tracking branch from remote."
|
||||||
|
git checkout -b stable --track origin/stable
|
||||||
|
else
|
||||||
|
echo "Creating new local stable branch at source SHA."
|
||||||
|
git checkout -b stable "${{ steps.meta.outputs.sha }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Reset stable to source SHA
|
||||||
|
run: |
|
||||||
|
git checkout stable
|
||||||
|
git reset --hard "${{ steps.meta.outputs.sha }}"
|
||||||
|
git status --short --branch
|
||||||
|
|
||||||
|
- name: Push stable (force-with-lease)
|
||||||
|
run: |
|
||||||
|
# Safer than --force; refuses if remote moved unexpectedly
|
||||||
|
git push origin stable --force-with-lease
|
||||||
|
|||||||
@@ -8,3 +8,4 @@ __screenshots__*
|
|||||||
*.diff
|
*.diff
|
||||||
npm/
|
npm/
|
||||||
.idea
|
.idea
|
||||||
|
**/LICENSE
|
||||||
|
|||||||
Vendored
+1
-1
@@ -1,3 +1,3 @@
|
|||||||
{
|
{
|
||||||
"recommendations": ["bradlc.vscode-tailwindcss", "denoland.vscode-deno"]
|
"recommendations": ["bradlc.vscode-tailwindcss", "biomejs.biome"]
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+4
-1
@@ -2,5 +2,8 @@
|
|||||||
"editor.formatOnSave": true,
|
"editor.formatOnSave": true,
|
||||||
"editor.codeActionsOnSave": {
|
"editor.codeActionsOnSave": {
|
||||||
"source.fixAll.biome": "explicit",
|
"source.fixAll.biome": "explicit",
|
||||||
}
|
},
|
||||||
|
"search.exclude": {
|
||||||
|
"**/i18n/locales/*-*/**": true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
+13
-3
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "meshtastic-web",
|
"name": "@meshtastic/web",
|
||||||
"version": "2.7.0-0",
|
"version": "2.7.0-0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Meshtastic web client monorepo",
|
"description": "Meshtastic web client monorepo",
|
||||||
@@ -25,7 +25,8 @@
|
|||||||
"check:fix": "biome check --write",
|
"check:fix": "biome check --write",
|
||||||
"build:all": "pnpm run --filter '*' build",
|
"build:all": "pnpm run --filter '*' build",
|
||||||
"clean:all": "pnpm run --filter '*' clean",
|
"clean:all": "pnpm run --filter '*' clean",
|
||||||
"publish:packages": "pnpm run --filter 'packages/transport-* packages/core' build"
|
"publish:packages": "pnpm run build --filter 'packages/transport-*'",
|
||||||
|
"test": "vitest"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@bufbuild/protobuf": "^2.6.1",
|
"@bufbuild/protobuf": "^2.6.1",
|
||||||
@@ -37,6 +38,15 @@
|
|||||||
"@types/node": "^22.16.4",
|
"@types/node": "^22.16.4",
|
||||||
"biome": "^0.3.3",
|
"biome": "^0.3.3",
|
||||||
"tsdown": "^0.13.4",
|
"tsdown": "^0.13.4",
|
||||||
"typescript": "^5.8.3"
|
"typescript": "^5.8.3",
|
||||||
|
"vitest": "^3.2.4"
|
||||||
|
},
|
||||||
|
"pnpm": {
|
||||||
|
"onlyBuiltDependencies": [
|
||||||
|
"@tailwindcss/oxide",
|
||||||
|
"core-js",
|
||||||
|
"esbuild",
|
||||||
|
"simple-git-hooks"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,27 +1,34 @@
|
|||||||
{
|
{
|
||||||
"name": "@meshtastic/core",
|
"name": "@meshtastic/core",
|
||||||
"version": "2.6.6",
|
"version": "2.6.6-1",
|
||||||
"description": "Core functionalities for Meshtastic web applications.",
|
"description": "Core functionalities for Meshtastic web applications.",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./mod.ts"
|
".": "./mod.ts"
|
||||||
},
|
},
|
||||||
|
"type": "module",
|
||||||
"main": "./dist/mod.mjs",
|
"main": "./dist/mod.mjs",
|
||||||
"module": "./dist/mod.mjs",
|
"module": "./dist/mod.mjs",
|
||||||
"types": "./dist/mod.d.mts",
|
"types": "./dist/mod.d.mts",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"tsdown": {
|
"tsdown": {
|
||||||
"entry": ["mod.ts"],
|
"entry": "mod.ts",
|
||||||
"dts": true,
|
"dts": true,
|
||||||
"format": ["esm"],
|
"format": ["esm"],
|
||||||
"splitting": false,
|
"splitting": false,
|
||||||
"clean": true
|
"clean": true
|
||||||
},
|
},
|
||||||
|
"files": [
|
||||||
|
"package.json",
|
||||||
|
"README.md",
|
||||||
|
"LICENSE",
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"prepack": "cp ../../LICENSE ./LICENSE",
|
"prepack": "cp ../../LICENSE ./LICENSE",
|
||||||
"clean": "rm -rf dist LICENSE",
|
"clean": "rm -rf dist LICENSE",
|
||||||
"build:npm": "tsdown",
|
"build:npm": "tsdown",
|
||||||
"publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public",
|
"publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public --no-git-checks",
|
||||||
"prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr",
|
"prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr",
|
||||||
"publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check"
|
"publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -626,7 +626,7 @@ export class MeshDevice {
|
|||||||
public async reboot(time: number): Promise<number> {
|
public async reboot(time: number): Promise<number> {
|
||||||
this.log.debug(
|
this.log.debug(
|
||||||
Emitter[Emitter.Reboot],
|
Emitter[Emitter.Reboot],
|
||||||
`🔌 Rebooting node ${time > 0 ? "now" : `in ${time} seconds`}`,
|
`🔌 Rebooting node ${time === 0 ? "now" : `in ${time} seconds`}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const reboot = create(Protobuf.Admin.AdminMessageSchema, {
|
const reboot = create(Protobuf.Admin.AdminMessageSchema, {
|
||||||
@@ -649,7 +649,7 @@ export class MeshDevice {
|
|||||||
public async rebootOta(time: number): Promise<number> {
|
public async rebootOta(time: number): Promise<number> {
|
||||||
this.log.debug(
|
this.log.debug(
|
||||||
Emitter[Emitter.RebootOta],
|
Emitter[Emitter.RebootOta],
|
||||||
`🔌 Rebooting into OTA mode ${time > 0 ? "now" : `in ${time} seconds`}`,
|
`🔌 Rebooting into OTA mode ${time === 0 ? "now" : `in ${time} seconds`}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
const rebootOta = create(Protobuf.Admin.AdminMessageSchema, {
|
const rebootOta = create(Protobuf.Admin.AdminMessageSchema, {
|
||||||
|
|||||||
@@ -330,6 +330,15 @@ export class EventSystem {
|
|||||||
PacketMetadata<Uint8Array>
|
PacketMetadata<Uint8Array>
|
||||||
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
> = new SimpleEventDispatcher<PacketMetadata<Uint8Array>>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fires when a new MeshPacket message containing a ClientNotification packet has been
|
||||||
|
* received from device
|
||||||
|
*
|
||||||
|
* @event onClientNotificationPacket
|
||||||
|
*/
|
||||||
|
public readonly onClientNotificationPacket: SimpleEventDispatcher<Protobuf.Mesh.ClientNotification> =
|
||||||
|
new SimpleEventDispatcher<Protobuf.Mesh.ClientNotification>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fires when the devices connection or configuration status changes
|
* Fires when the devices connection or configuration status changes
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -209,6 +209,18 @@ export const decodePacket = (device: MeshDevice) =>
|
|||||||
break;
|
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: {
|
default: {
|
||||||
device.log.warn(
|
device.log.warn(
|
||||||
Types.Emitter[Types.Emitter.HandleFromRadio],
|
Types.Emitter[Types.Emitter.HandleFromRadio],
|
||||||
|
|||||||
@@ -8,8 +8,20 @@
|
|||||||
"main": "./dist/mod.mjs",
|
"main": "./dist/mod.mjs",
|
||||||
"module": "./dist/mod.mjs",
|
"module": "./dist/mod.mjs",
|
||||||
"types": "./dist/mod.d.mts",
|
"types": "./dist/mod.d.mts",
|
||||||
"files": ["dist/*", "mod.ts", "README.md", "../../LICENSE"],
|
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
|
"tsdown": {
|
||||||
|
"entry": "mod.ts",
|
||||||
|
"dts": true,
|
||||||
|
"format": ["esm"],
|
||||||
|
"splitting": false,
|
||||||
|
"clean": true
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"package.json",
|
||||||
|
"README.md",
|
||||||
|
"LICENSE",
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"prepack": "cp ../../LICENSE ./LICENSE",
|
"prepack": "cp ../../LICENSE ./LICENSE",
|
||||||
@@ -18,5 +30,8 @@
|
|||||||
"publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public",
|
"publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public",
|
||||||
"prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr",
|
"prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr",
|
||||||
"publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check"
|
"publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@meshtastic/core": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,37 @@
|
|||||||
{
|
{
|
||||||
"name": "@meshtastic/transport-http",
|
"name": "@meshtastic/transport-http",
|
||||||
"version": "0.2.3",
|
"version": "0.2.3-2",
|
||||||
"description": "A transport layer for Meshtastic applications using HTTP.",
|
"description": "A transport layer for Meshtastic applications using HTTP.",
|
||||||
"exports": {".": "./mod.ts"},
|
"exports": {".": "./mod.ts"},
|
||||||
"main": "./dist/mod.mjs",
|
"type": "module",
|
||||||
"module": "./dist/mod.mjs",
|
"files": [
|
||||||
"types": "./dist/mod.d.mts",
|
|
||||||
"license": "GPL-3.0-only",
|
|
||||||
"tsdown": {
|
|
||||||
"entry": ["mod.ts"],
|
|
||||||
"dts": true,
|
|
||||||
"format": ["esm"],
|
|
||||||
"splitting": false,
|
|
||||||
"clean": true
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"package.json",
|
"package.json",
|
||||||
"README.md",
|
"README.md",
|
||||||
"LICENSE",
|
"LICENSE",
|
||||||
"dist"
|
"dist"
|
||||||
],
|
],
|
||||||
|
"main": "./dist/mod.mjs",
|
||||||
|
"module": "./dist/mod.mjs",
|
||||||
|
"types": "./dist/mod.d.mts",
|
||||||
|
"license": "GPL-3.0-only",
|
||||||
|
"tsdown": {
|
||||||
|
"entry": "mod.ts",
|
||||||
|
"dts": true,
|
||||||
|
"format": ["esm"],
|
||||||
|
"splitting": false,
|
||||||
|
"clean": true
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"preinstall": "npx only-allow pnpm",
|
"preinstall": "npx only-allow pnpm",
|
||||||
"prepack": "cp ../../LICENSE ./LICENSE",
|
"prepack": "cp ../../LICENSE ./LICENSE",
|
||||||
"clean": "rm -rf dist LICENSE",
|
"clean": "rm -rf dist LICENSE",
|
||||||
"build:npm": "tsdown",
|
"build:npm": "tsdown",
|
||||||
"publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public",
|
|
||||||
|
"publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public --no-git-checks",
|
||||||
"prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr",
|
"prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr",
|
||||||
"publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check"
|
"publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@meshtastic/core": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
# @meshtastic/transport-node-serial
|
||||||
|
|
||||||
|
[](https://jsr.io/@meshtastic/transport-node-serial)
|
||||||
|
[](https://github.com/meshtastic/js/actions/workflows/ci.yml)
|
||||||
|
[](https://cla-assistant.io/meshtastic/meshtastic.js)
|
||||||
|
[](https://opencollective.com/meshtastic/)
|
||||||
|
[](https://vercel.com?utm_source=meshtastic&utm_campaign=oss)
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`@meshtastic/transport-noden-node` Provides Serial transport (Node) for Meshtastic
|
||||||
|
devices. Installation instructions are available at
|
||||||
|
[JSR](https://jsr.io/@meshtastic/transport-node-serial)
|
||||||
|
[NPM](https://www.npmjs.com/package/@meshtastic/transport-node-serial)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { MeshDevice } from "@meshtastic/core";
|
||||||
|
import { TransportNodeSerial } from "@meshtastic/transport-node-serial";
|
||||||
|
|
||||||
|
const transport = await TransportNodeSerial.create("/dev/cu.usbserial-0001");
|
||||||
|
const device = new MeshDevice(transport);
|
||||||
|
```
|
||||||
|
|
||||||
|
## Stats
|
||||||
|
|
||||||
|

|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { TransportNodeSerial } from "./src/transport.ts";
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
{
|
||||||
|
"name": "@meshtastic/transport-node-serial",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "NodeJS-specific serial transport layer for Meshtastic web applications.",
|
||||||
|
"exports": {
|
||||||
|
".": "./dist/mod.mjs"
|
||||||
|
},
|
||||||
|
"main": "./dist/mod.mjs",
|
||||||
|
"module": "./dist/mod.mjs",
|
||||||
|
"types": "./dist/mod.d.mts",
|
||||||
|
|
||||||
|
"license": "GPL-3.0-only",
|
||||||
|
"tsdown": {
|
||||||
|
"entry": "mod.ts",
|
||||||
|
"dts": true,
|
||||||
|
"format": ["esm"],
|
||||||
|
"splitting": false,
|
||||||
|
"clean": true
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"package.json",
|
||||||
|
"README.md",
|
||||||
|
"LICENSE",
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"preinstall": "npx only-allow pnpm",
|
||||||
|
"prepack": "cp ../../LICENSE ./LICENSE",
|
||||||
|
"clean": "rm -rf dist LICENSE",
|
||||||
|
"build:npm": "tsdown",
|
||||||
|
"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:*",
|
||||||
|
"serialport": "^13.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
import { Readable, Writable } from "node:stream";
|
||||||
|
import type { Types } from "@meshtastic/core";
|
||||||
|
import { Utils } from "@meshtastic/core";
|
||||||
|
import { SerialPort } from "serialport";
|
||||||
|
|
||||||
|
export class TransportNodeSerial implements Types.Transport {
|
||||||
|
private readonly _toDevice: WritableStream<Uint8Array>;
|
||||||
|
private readonly _fromDevice: ReadableStream<Types.DeviceOutput>;
|
||||||
|
private port: SerialPort | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates and connects a new TransportNode instance.
|
||||||
|
* @param path - Path to the serial device
|
||||||
|
* @param baudRate - The port number for the TCP connection (defaults to 4403).
|
||||||
|
* @returns A promise that resolves with a connected TransportNode instance.
|
||||||
|
*/
|
||||||
|
public static create(path: string, baudRate = 115200): Promise<TransportNodeSerial> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const port = new SerialPort({
|
||||||
|
path,
|
||||||
|
baudRate,
|
||||||
|
autoOpen: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onError = (err: Error) => {
|
||||||
|
port.close();
|
||||||
|
reject(err);
|
||||||
|
};
|
||||||
|
|
||||||
|
port.once("error", onError);
|
||||||
|
port.on("open", () => {
|
||||||
|
port.removeListener("error", onError);
|
||||||
|
resolve(new TransportNodeSerial(port));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Constructs a new TransportNode.
|
||||||
|
* @param port - An active Node.js SerialPort connection.
|
||||||
|
*/
|
||||||
|
constructor(port: SerialPort) {
|
||||||
|
this.port = port;
|
||||||
|
this.port.on("error", (err) => {
|
||||||
|
console.error("Serial port connection error:", err);
|
||||||
|
});
|
||||||
|
|
||||||
|
const fromDeviceSource = Readable.toWeb(
|
||||||
|
port,
|
||||||
|
) as ReadableStream<Uint8Array>;
|
||||||
|
this._fromDevice = fromDeviceSource.pipeThrough(Utils.fromDeviceStream());
|
||||||
|
|
||||||
|
// Stream for data going FROM the application TO the Meshtastic device.
|
||||||
|
const toDeviceTransform = Utils.toDeviceStream;
|
||||||
|
this._toDevice = toDeviceTransform.writable;
|
||||||
|
|
||||||
|
// The readable end of the transform is then piped to the Node.js SerialPort connection.
|
||||||
|
// A similar assertion is needed here because `Writable.toWeb` also returns
|
||||||
|
// a generically typed stream (`WritableStream<any>`).
|
||||||
|
toDeviceTransform.readable
|
||||||
|
.pipeTo(Writable.toWeb(port) as WritableStream<Uint8Array>)
|
||||||
|
.catch((err) => {
|
||||||
|
console.error("Error piping data to serial port:", err);
|
||||||
|
this.port.close(err as Error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The WritableStream to send data to the Meshtastic device.
|
||||||
|
*/
|
||||||
|
public get toDevice(): WritableStream<Uint8Array> {
|
||||||
|
return this._toDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The ReadableStream to receive data from the Meshtastic device.
|
||||||
|
*/
|
||||||
|
public get fromDevice(): ReadableStream<Types.DeviceOutput> {
|
||||||
|
return this._fromDevice;
|
||||||
|
}
|
||||||
|
|
||||||
|
disconnect() {
|
||||||
|
this.port.close();
|
||||||
|
this.port = undefined;
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "ESNext",
|
||||||
|
"target": "ES2020",
|
||||||
|
"declaration": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"emitDeclarationOnly": false,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
},
|
||||||
|
"include": ["src"]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
{
|
{
|
||||||
"name": "@meshtastic/transport-node",
|
"name": "@meshtastic/transport-node",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1-1",
|
||||||
"description": "NodeJS-specific transport layer for Meshtastic web applications.",
|
"description": "NodeJS-specific transport layer for Meshtastic web applications.",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./mod.ts"
|
".": "./mod.ts"
|
||||||
},
|
},
|
||||||
|
"type": "module",
|
||||||
"main": "./dist/mod.mjs",
|
"main": "./dist/mod.mjs",
|
||||||
"module": "./dist/mod.mjs",
|
"module": "./dist/mod.mjs",
|
||||||
"types": "./dist/mod.d.mts",
|
"types": "./dist/mod.d.mts",
|
||||||
|
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"tsdown": {
|
"tsdown": {
|
||||||
"entry": ["mod.ts"],
|
"entry": "mod.ts",
|
||||||
"dts": true,
|
"dts": true,
|
||||||
"format": ["esm"],
|
"format": ["esm"],
|
||||||
"splitting": false,
|
"splitting": false,
|
||||||
@@ -28,8 +29,11 @@
|
|||||||
"prepack": "cp ../../LICENSE ./LICENSE",
|
"prepack": "cp ../../LICENSE ./LICENSE",
|
||||||
"clean": "rm -rf dist LICENSE",
|
"clean": "rm -rf dist LICENSE",
|
||||||
"build:npm": "tsdown",
|
"build:npm": "tsdown",
|
||||||
"publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public",
|
"publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public --no-git-checks",
|
||||||
"prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr",
|
"prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr",
|
||||||
"publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check"
|
"publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@meshtastic/core": "workspace:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "@meshtastic/transport-web-bluetooth",
|
"name": "@meshtastic/transport-web-bluetooth",
|
||||||
"version": "0.1.4",
|
"version": "0.1.4-1",
|
||||||
"description": "A transport layer for Meshtastic applications using Web Bluetooth.",
|
"description": "A transport layer for Meshtastic applications using Web Bluetooth.",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./mod.ts"
|
".": "./mod.ts"
|
||||||
},
|
},
|
||||||
|
"type": "module",
|
||||||
"main": "./dist/mod.mjs",
|
"main": "./dist/mod.mjs",
|
||||||
"module": "./dist/mod.mjs",
|
"module": "./dist/mod.mjs",
|
||||||
"types": "./dist/mod.d.mts",
|
"types": "./dist/mod.d.mts",
|
||||||
"files": ["dist/*", "mod.ts", "README.md", "../../LICENSE"],
|
"files": [
|
||||||
|
"package.json",
|
||||||
|
"README.md",
|
||||||
|
"LICENSE",
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"tsdown": {
|
"tsdown": {
|
||||||
"entry": ["mod.ts"],
|
"entry": "mod.ts",
|
||||||
"dts": true,
|
"dts": true,
|
||||||
"format": ["esm"],
|
"format": ["esm"],
|
||||||
"splitting": false,
|
"splitting": false,
|
||||||
@@ -22,12 +28,12 @@
|
|||||||
"prepack": "cp ../../LICENSE ./LICENSE",
|
"prepack": "cp ../../LICENSE ./LICENSE",
|
||||||
"clean": "rm -rf dist LICENSE",
|
"clean": "rm -rf dist LICENSE",
|
||||||
"build:npm": "tsdown",
|
"build:npm": "tsdown",
|
||||||
"publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public",
|
"publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public --no-git-checks",
|
||||||
"prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr",
|
"prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr",
|
||||||
"publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check"
|
"publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/web-bluetooth": "npm:@types/web-bluetooth@^0.0.20"
|
"@types/web-bluetooth": "npm:@types/web-bluetooth@^0.0.20",
|
||||||
}
|
"@meshtastic/core": "workspace:*"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,23 @@
|
|||||||
{
|
{
|
||||||
"name": "@meshtastic/transport-web-serial",
|
"name": "@meshtastic/transport-web-serial",
|
||||||
"version": "0.2.3",
|
"version": "0.2.3-1",
|
||||||
"description": "A transport layer for Meshtastic applications using Web Serial API.",
|
"description": "A transport layer for Meshtastic applications using Web Serial API.",
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./mod.ts"
|
".": "./mod.ts"
|
||||||
},
|
},
|
||||||
|
"type": "module",
|
||||||
"main": "./dist/mod.mjs",
|
"main": "./dist/mod.mjs",
|
||||||
"module": "./dist/mod.mjs",
|
"module": "./dist/mod.mjs",
|
||||||
"types": "./dist/mod.d.mts",
|
"types": "./dist/mod.d.mts",
|
||||||
"files": ["dist/*", "mod.ts", "README.md", "../../LICENSE"],
|
"files": [
|
||||||
|
"package.json",
|
||||||
|
"README.md",
|
||||||
|
"LICENSE",
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
"tsdown": {
|
"tsdown": {
|
||||||
"entry": ["mod.ts"],
|
"entry": "mod.ts",
|
||||||
"dts": true,
|
"dts": true,
|
||||||
"format": ["esm"],
|
"format": ["esm"],
|
||||||
"splitting": false,
|
"splitting": false,
|
||||||
@@ -22,11 +28,12 @@
|
|||||||
"prepack": "cp ../../LICENSE ./LICENSE",
|
"prepack": "cp ../../LICENSE ./LICENSE",
|
||||||
"clean": "rm -rf dist LICENSE",
|
"clean": "rm -rf dist LICENSE",
|
||||||
"build:npm": "tsdown",
|
"build:npm": "tsdown",
|
||||||
"publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public",
|
"publish:npm": "pnpm clean && pnpm build:npm && pnpm publish --access public --no-git-checks",
|
||||||
"prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr",
|
"prepare:jsr": "rm -rf dist && pnpm dlx pkg-to-jsr",
|
||||||
"publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check"
|
"publish:jsr": "pnpm run prepack && pnpm prepare:jsr && deno publish --allow-dirty --no-check"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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:*"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
# Contributing to Meshtastic Web
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to **Meshtastic Web**! 🎉
|
||||||
|
We welcome all contributions—whether it’s fixing a typo, improving documentation, adding new features, or reporting bugs. This document outlines how to get started and the conventions we follow.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📋 Code of Conduct
|
||||||
|
We follow the [Meshtastic Code of Conduct](https://meshtastic.org/docs/legal/conduct/).
|
||||||
|
Please make sure you are familiar with it before contributing.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Getting Started
|
||||||
|
Before making changes, please take some time to explore the repository and its monorepo structure.
|
||||||
|
Understanding how the packages are organized will make it much easier to contribute effectively.
|
||||||
|
|
||||||
|
[Meshtastic Web](https://github.com/meshtastic/web/)
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
- [Node.js](https://nodejs.org/) (v22 or later)
|
||||||
|
- [pnpm](https://pnpm.io/) (v10.14.x or later)
|
||||||
|
- Git
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
Clone the repo and install dependencies:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/meshtastic/web.git meshtastic-web
|
||||||
|
cd meshtastic-web
|
||||||
|
pnpm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development
|
||||||
|
Start the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @meshtastic/web dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Once running, the site will be available at:
|
||||||
|
👉 **http://localhost:3000**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🗂 Repository Structure
|
||||||
|
Meshtastic Web uses a **monorepo** setup managed with **pnpm workspaces**:
|
||||||
|
|
||||||
|
```
|
||||||
|
/packages
|
||||||
|
├─ web # React frontend
|
||||||
|
├─ core # Shared types & logic
|
||||||
|
├─ transport-* # Transport layer packages
|
||||||
|
└─ ...other packages
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✅ Contribution Workflow
|
||||||
|
|
||||||
|
1. **Fork the repo** and create your branch from `main`.
|
||||||
|
|
||||||
|
### Branch Naming
|
||||||
|
- Use [Conventional Commit](https://www.conventionalcommits.org/) style for your branch names:
|
||||||
|
```
|
||||||
|
feat/add-project-filter
|
||||||
|
fix/storage-service
|
||||||
|
chore/update-ci-cache
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Make your changes locally** and verify that the app runs as expected at `http://localhost:3000`.
|
||||||
|
|
||||||
|
3. **Commit your changes** with a descriptive commit message that follows the [Conventional Commits](https://www.conventionalcommits.org/) style.
|
||||||
|
|
||||||
|
4. **Open a Pull Request (PR)** from your fork's branch to the main repository's `main` branch on GitHub:
|
||||||
|
- Clearly describe the problem and solution.
|
||||||
|
- Reference related issues (e.g., `Fixes #123`).
|
||||||
|
- Keep PRs focused on a single feature or fix.
|
||||||
|
- Complete all fields in the PR template.
|
||||||
|
- Tag a **Meshtastic Web developer** in the PR for review.
|
||||||
|
|
||||||
|
5. **CI/CD**:
|
||||||
|
- Our GitHub Actions workflows handle builds, linting, and packaging automatically.
|
||||||
|
- All checks must pass before merge.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🌍 Internationalization (i18n)
|
||||||
|
|
||||||
|
Meshtastic Web supports multiple languages. If your changes introduce **new user-facing strings**:
|
||||||
|
|
||||||
|
- Add them to the **`en.json`** file.
|
||||||
|
- Do **not** hardcode English strings directly in components.
|
||||||
|
- This ensures they can be translated into other languages.
|
||||||
|
|
||||||
|
🔗 See these guides for more details:
|
||||||
|
- [i18n Developer Guide](https://github.com/meshtastic/web/blob/main/packages/web/CONTRIBUTING_I18N_DEVELOPER_GUIDE.md)
|
||||||
|
- [Translation Contribution Guide](https://github.com/meshtastic/web/blob/main/packages/web/CONTRIBUTING_TRANSLATIONS.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧪 Testing
|
||||||
|
Tests are written with [Vitest](https://vitest.dev/).
|
||||||
|
|
||||||
|
Run all tests locally with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm --filter @meshtastic/web test
|
||||||
|
```
|
||||||
|
|
||||||
|
Please include tests for new features and bug fixes whenever possible.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 Commit Messages
|
||||||
|
We use **Conventional Commits**:
|
||||||
|
|
||||||
|
- `feat:` – a new feature
|
||||||
|
- `fix:` – a bug fix
|
||||||
|
- `docs:` – documentation changes
|
||||||
|
- `chore:` – maintenance, dependencies, build scripts
|
||||||
|
- `refactor:` – code restructuring without feature changes
|
||||||
|
- `test:` – adding or updating tests
|
||||||
|
- `ci:` – CI/CD changes
|
||||||
|
|
||||||
|
Example:
|
||||||
|
```
|
||||||
|
feat: add toast notification system
|
||||||
|
fix: correct caching issue in storage service
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 💡 Tips for Contributors
|
||||||
|
- Keep PRs **small, focused, and atomic**.
|
||||||
|
- Discuss larger changes with the team on [Discord](https://discord.gg/meshtastic) before starting work.
|
||||||
|
- If unsure, open a draft PR for early feedback.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🙌 Community
|
||||||
|
Contributors are the heart of Meshtastic ❤️.
|
||||||
|
Join the conversation:
|
||||||
|
- [Discord](https://discord.gg/meshtastic)
|
||||||
|
- [GitHub Discussions](https://github.com/meshtastic/web/discussions)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📜 License
|
||||||
|
By contributing, you agree that your contributions will be licensed under the [GPL-3.0-only License](../../LICENSE).
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
FROM nginx:1.27-alpine
|
FROM nginx:1.27-alpine
|
||||||
|
|
||||||
RUN rm -r /usr/share/nginx/html \
|
RUN rm -r /usr/share/nginx/html \
|
||||||
&& mkdir -p /usr/share/nginx/html \
|
&& mkdir -p /usr/share/nginx/html \
|
||||||
&& mkdir -p /etc/nginx/conf.d
|
&& mkdir -p /etc/nginx/conf.d
|
||||||
|
|
||||||
WORKDIR /usr/share/nginx/html
|
WORKDIR /usr/share/nginx/html
|
||||||
|
|
||||||
ADD dist .
|
ADD ./dist .
|
||||||
|
|
||||||
COPY ./infra/default.conf /etc/nginx/conf.d/default.conf
|
COPY ./infra/default.conf /etc/nginx/conf.d/default.conf
|
||||||
|
|
||||||
|
|||||||
@@ -1,42 +1,51 @@
|
|||||||
server {
|
server {
|
||||||
listen 8080;
|
listen 8080;
|
||||||
server_name localhost;
|
server_name localhost;
|
||||||
|
|
||||||
root /usr/share/nginx/html;
|
root /usr/share/nginx/html;
|
||||||
index index.html index.htm;
|
index index.html;
|
||||||
|
|
||||||
location / {
|
location ~* \.(?:js|jsx|mjs|ts|tsx|css|png|jpg|jpeg|gif|ico|webp|avif|svg|ttf|otf|woff|woff2|map)$ {
|
||||||
try_files $uri $uri/ /index.html;
|
access_log off;
|
||||||
}
|
etag on;
|
||||||
|
|
||||||
error_page 500 502 503 504 /50x.html;
|
expires 3M;
|
||||||
location = /50x.html {
|
add_header Cache-Control "public, immutable";
|
||||||
internal;
|
try_files $uri =404;
|
||||||
}
|
}
|
||||||
|
|
||||||
location ~ /\.ht {
|
location = /index.html {
|
||||||
deny all;
|
etag on;
|
||||||
}
|
add_header Cache-Control "no-cache";
|
||||||
|
}
|
||||||
|
|
||||||
gzip on;
|
location / {
|
||||||
gzip_disable "msie6";
|
try_files $uri $uri/ /index.html;
|
||||||
|
}
|
||||||
|
|
||||||
gzip_vary on;
|
error_page 500 502 503 504 /50x.html;
|
||||||
gzip_proxied any;
|
location = /50x.html { internal; }
|
||||||
gzip_comp_level 6;
|
|
||||||
gzip_buffers 16 8k;
|
location ~ /\.ht { deny all; }
|
||||||
gzip_http_version 1.1;
|
|
||||||
gzip_types
|
gzip on;
|
||||||
text/plain
|
gzip_disable "msie6";
|
||||||
text/css
|
gzip_vary on;
|
||||||
text/xml
|
gzip_proxied any;
|
||||||
text/javascript
|
gzip_comp_level 6;
|
||||||
application/javascript
|
gzip_buffers 16 8k;
|
||||||
application/x-javascript
|
gzip_http_version 1.1;
|
||||||
application/json
|
gzip_types
|
||||||
application/xml
|
text/plain
|
||||||
application/xml+rss
|
text/css
|
||||||
font/ttf
|
text/xml
|
||||||
font/otf
|
text/javascript
|
||||||
image/svg+xml;
|
application/javascript
|
||||||
|
application/x-javascript
|
||||||
|
application/json
|
||||||
|
application/xml
|
||||||
|
application/xml+rss
|
||||||
|
font/ttf
|
||||||
|
font/otf
|
||||||
|
image/svg+xml;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "meshtastic-web",
|
"name": "meshtastic-web",
|
||||||
"version": "2.7.0-0",
|
"version": "2.6.6-0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"description": "Meshtastic web client",
|
"description": "Meshtastic web client",
|
||||||
"license": "GPL-3.0-only",
|
"license": "GPL-3.0-only",
|
||||||
@@ -22,6 +22,7 @@
|
|||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"ts:check": "bun run tsc --noEmit",
|
"ts:check": "bun run tsc --noEmit",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
|
"docker:build": "docker build -t meshtastic-web:latest -f ./infra/Containerfile .",
|
||||||
"generate:routes": "bun @tanstack/router-cli generate --outDir src/ routes --rootRoutePath /",
|
"generate:routes": "bun @tanstack/router-cli generate --outDir src/ routes --rootRoutePath /",
|
||||||
"package": "gzipper c -i html,js,css,png,ico,svg,json,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ ."
|
"package": "gzipper c -i html,js,css,png,ico,svg,json,webmanifest,txt dist dist/output && tar -cvf dist/build.tar -C ./dist/output/ ."
|
||||||
},
|
},
|
||||||
@@ -29,9 +30,9 @@
|
|||||||
"@bufbuild/protobuf": "^2.6.0",
|
"@bufbuild/protobuf": "^2.6.0",
|
||||||
"@hookform/resolvers": "^5.1.1",
|
"@hookform/resolvers": "^5.1.1",
|
||||||
"@meshtastic/core": "workspace:*",
|
"@meshtastic/core": "workspace:*",
|
||||||
"@meshtastic/transport-http": "npm:@jsr/meshtastic__transport-http",
|
"@meshtastic/transport-http": "workspace:*",
|
||||||
"@meshtastic/transport-web-bluetooth": "npm:@jsr/meshtastic__transport-web-bluetooth",
|
"@meshtastic/transport-web-bluetooth": "workspace:*",
|
||||||
"@meshtastic/transport-web-serial": "npm:@jsr/meshtastic__transport-web-serial",
|
"@meshtastic/transport-web-serial": "workspace:*",
|
||||||
"@noble/curves": "^1.9.2",
|
"@noble/curves": "^1.9.2",
|
||||||
"@radix-ui/react-accordion": "^1.2.11",
|
"@radix-ui/react-accordion": "^1.2.11",
|
||||||
"@radix-ui/react-checkbox": "^1.3.2",
|
"@radix-ui/react-checkbox": "^1.3.2",
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
"save": "Запис",
|
"save": "Запис",
|
||||||
"scanQr": "Сканиране на QR кода",
|
"scanQr": "Сканиране на QR кода",
|
||||||
"traceRoute": "Trace Route",
|
"traceRoute": "Trace Route",
|
||||||
"submit": "Submit"
|
"submit": "Изпращане"
|
||||||
},
|
},
|
||||||
"app": {
|
"app": {
|
||||||
"title": "Meshtastic",
|
"title": "Meshtastic",
|
||||||
|
|||||||
@@ -18,22 +18,22 @@
|
|||||||
"description": "Настройки за устройството",
|
"description": "Настройки за устройството",
|
||||||
"buttonPin": {
|
"buttonPin": {
|
||||||
"description": "Button pin override",
|
"description": "Button pin override",
|
||||||
"label": "Button Pin"
|
"label": "Пин за бутон"
|
||||||
},
|
},
|
||||||
"buzzerPin": {
|
"buzzerPin": {
|
||||||
"description": "Buzzer pin override",
|
"description": "Buzzer pin override",
|
||||||
"label": "Buzzer Pin"
|
"label": "Пин за зумер"
|
||||||
},
|
},
|
||||||
"disableTripleClick": {
|
"disableTripleClick": {
|
||||||
"description": "Disable triple click",
|
"description": "Дезактивиране на трикратното щракване",
|
||||||
"label": "Disable Triple Click"
|
"label": "Дезактивиране на трикратното щракване"
|
||||||
},
|
},
|
||||||
"doubleTapAsButtonPress": {
|
"doubleTapAsButtonPress": {
|
||||||
"description": "Treat double tap as button press",
|
"description": "Treat double tap as button press",
|
||||||
"label": "Double Tap as Button Press"
|
"label": "Double Tap as Button Press"
|
||||||
},
|
},
|
||||||
"ledHeartbeatDisabled": {
|
"ledHeartbeatDisabled": {
|
||||||
"description": "Disable default blinking LED",
|
"description": "Дезактивиране на мигащия светодиод по подразбиране",
|
||||||
"label": "LED Heartbeat Disabled"
|
"label": "LED Heartbeat Disabled"
|
||||||
},
|
},
|
||||||
"nodeInfoBroadcastInterval": {
|
"nodeInfoBroadcastInterval": {
|
||||||
@@ -138,7 +138,7 @@
|
|||||||
"label": "Отместване на честотата"
|
"label": "Отместване на честотата"
|
||||||
},
|
},
|
||||||
"frequencySlot": {
|
"frequencySlot": {
|
||||||
"description": "LoRa frequency channel number",
|
"description": "Номер на LoRa честотен канал",
|
||||||
"label": "Честотен слот"
|
"label": "Честотен слот"
|
||||||
},
|
},
|
||||||
"hopLimit": {
|
"hopLimit": {
|
||||||
@@ -277,7 +277,7 @@
|
|||||||
},
|
},
|
||||||
"enablePin": {
|
"enablePin": {
|
||||||
"description": "GPS module enable pin override",
|
"description": "GPS module enable pin override",
|
||||||
"label": "Enable Pin"
|
"label": "Активиране на пин"
|
||||||
},
|
},
|
||||||
"fixedPosition": {
|
"fixedPosition": {
|
||||||
"description": "Don't report GPS position, but a manually-specified one",
|
"description": "Don't report GPS position, but a manually-specified one",
|
||||||
@@ -341,7 +341,7 @@
|
|||||||
},
|
},
|
||||||
"ina219Address": {
|
"ina219Address": {
|
||||||
"description": "Address of the INA219 battery monitor",
|
"description": "Address of the INA219 battery monitor",
|
||||||
"label": "INA219 Address"
|
"label": "INA219 адрес"
|
||||||
},
|
},
|
||||||
"lightSleepDuration": {
|
"lightSleepDuration": {
|
||||||
"description": "How long the device will be in light sleep for",
|
"description": "How long the device will be in light sleep for",
|
||||||
@@ -353,7 +353,7 @@
|
|||||||
},
|
},
|
||||||
"noConnectionBluetoothDisabled": {
|
"noConnectionBluetoothDisabled": {
|
||||||
"description": "If the device does not receive a Bluetooth connection, the BLE radio will be disabled after this long",
|
"description": "If the device does not receive a Bluetooth connection, the BLE radio will be disabled after this long",
|
||||||
"label": "No Connection Bluetooth Disabled"
|
"label": "Няма връзка Bluetooth е дезактивиран"
|
||||||
},
|
},
|
||||||
"powerSavingEnabled": {
|
"powerSavingEnabled": {
|
||||||
"description": "Select if powered from a low-current source (i.e. solar), to minimize power consumption as much as possible.",
|
"description": "Select if powered from a low-current source (i.e. solar), to minimize power consumption as much as possible.",
|
||||||
@@ -390,10 +390,10 @@
|
|||||||
},
|
},
|
||||||
"managed": {
|
"managed": {
|
||||||
"description": "If enabled, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless at least one suitable Remote Admin node has been setup, and the public key is stored in one of the fields above.",
|
"description": "If enabled, device configuration options are only able to be changed remotely by a Remote Admin node via admin messages. Do not enable this option unless at least one suitable Remote Admin node has been setup, and the public key is stored in one of the fields above.",
|
||||||
"label": "Managed"
|
"label": "Управляван"
|
||||||
},
|
},
|
||||||
"privateKey": {
|
"privateKey": {
|
||||||
"description": "Used to create a shared key with a remote device",
|
"description": "Използва се за създаване на споделен ключ с отдалечено устройство",
|
||||||
"label": "Частен ключ"
|
"label": "Частен ключ"
|
||||||
},
|
},
|
||||||
"publicKey": {
|
"publicKey": {
|
||||||
@@ -418,7 +418,7 @@
|
|||||||
},
|
},
|
||||||
"adminSettings": {
|
"adminSettings": {
|
||||||
"description": "Настройки за Admin",
|
"description": "Настройки за Admin",
|
||||||
"label": "Admin Settings"
|
"label": "Администраторски настройки"
|
||||||
},
|
},
|
||||||
"loggingSettings": {
|
"loggingSettings": {
|
||||||
"description": "Settings for Logging",
|
"description": "Settings for Logging",
|
||||||
|
|||||||
@@ -9,10 +9,10 @@
|
|||||||
"shortName": "Кратко име",
|
"shortName": "Кратко име",
|
||||||
"title": "Промяна на името на устройството",
|
"title": "Промяна на името на устройството",
|
||||||
"validation": {
|
"validation": {
|
||||||
"longNameMax": "Long name must not be more than 40 characters",
|
"longNameMax": "Дългото име не трябва да е повече от 40 знака",
|
||||||
"shortNameMax": "Short name must not be more than 4 characters",
|
"shortNameMax": "Краткото име не трябва да е повече от 4 знака",
|
||||||
"longNameMin": "Long name must have at least 1 character",
|
"longNameMin": "Дългото име трябва да съдържа поне 1 символ",
|
||||||
"shortNameMin": "Short name must have at least 1 character"
|
"shortNameMin": "Краткото име трябва да съдържа поне 1 символ"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"import": {
|
"import": {
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"channelPrefix": "Канал: ",
|
"channelPrefix": "Канал: ",
|
||||||
"channelSetUrl": "Channel Set/QR Code URL",
|
"channelSetUrl": "Channel Set/QR Code URL",
|
||||||
"channels": "Канали:",
|
"channels": "Канали:",
|
||||||
"usePreset": "Use Preset?",
|
"usePreset": "Използване на предварително зададени настройки?",
|
||||||
"title": "Import Channel Set"
|
"title": "Import Channel Set"
|
||||||
},
|
},
|
||||||
"locationResponse": {
|
"locationResponse": {
|
||||||
@@ -68,11 +68,11 @@
|
|||||||
"bluetoothConnection": {
|
"bluetoothConnection": {
|
||||||
"noDevicesPaired": "Все още няма сдвоени устройства.",
|
"noDevicesPaired": "Все още няма сдвоени устройства.",
|
||||||
"newDeviceButton": "Ново устройство",
|
"newDeviceButton": "Ново устройство",
|
||||||
"connectionFailed": "Connection failed",
|
"connectionFailed": "Връзката е неуспешна",
|
||||||
"deviceDisconnected": "Device disconnected",
|
"deviceDisconnected": "Устройството не е свързано",
|
||||||
"unknownDevice": "Неизвестно устройство",
|
"unknownDevice": "Неизвестно устройство",
|
||||||
"errorLoadingDevices": "Error loading devices",
|
"errorLoadingDevices": "Грешка при зареждане на устройствата",
|
||||||
"unknownErrorLoadingDevices": "Unknown error loading devices"
|
"unknownErrorLoadingDevices": "Неизвестна грешка при зареждане на устройствата"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"requiresFeatures": "Този тип връзка изисква <0></0>. Моля, използвайте поддържан браузър, като Chrome или Edge.",
|
"requiresFeatures": "Този тип връзка изисква <0></0>. Моля, използвайте поддържан браузър, като Chrome или Edge.",
|
||||||
@@ -116,7 +116,7 @@
|
|||||||
"pkiBackupReminder": {
|
"pkiBackupReminder": {
|
||||||
"description": "Препоръчваме редовно да правите резервни копия на данните с вашите ключове. Искате ли да направите резервно копие сега?",
|
"description": "Препоръчваме редовно да правите резервни копия на данните с вашите ключове. Искате ли да направите резервно копие сега?",
|
||||||
"title": "Напомняне за резервно копие",
|
"title": "Напомняне за резервно копие",
|
||||||
"remindLaterPrefix": "Remind me in",
|
"remindLaterPrefix": "Напомни ми в",
|
||||||
"remindNever": "Никога не ми напомняй",
|
"remindNever": "Никога не ми напомняй",
|
||||||
"backupNow": "Създаване на резервно копие сега"
|
"backupNow": "Създаване на резервно копие сега"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
{
|
{
|
||||||
"page": {
|
"page": {
|
||||||
"title": "Съобщения: {{chatName}}",
|
"title": "Съобщения: {{chatName}}",
|
||||||
"placeholder": "Enter Message"
|
"placeholder": "Въведете съобщение"
|
||||||
},
|
},
|
||||||
"emptyState": {
|
"emptyState": {
|
||||||
"title": "Изберете чат",
|
"title": "Изберете чат",
|
||||||
"text": "Все още няма съобщения."
|
"text": "Все още няма съобщения."
|
||||||
},
|
},
|
||||||
"selectChatPrompt": {
|
"selectChatPrompt": {
|
||||||
"text": "Select a channel or node to start messaging."
|
"text": "Изберете канал или възел, за да стартирате съобщения."
|
||||||
},
|
},
|
||||||
"sendMessage": {
|
"sendMessage": {
|
||||||
"placeholder": "Въведете Вашето съобщение тук...",
|
"placeholder": "Въведете Вашето съобщение тук...",
|
||||||
@@ -24,16 +24,16 @@
|
|||||||
"displayText": "Съобщението е доставено"
|
"displayText": "Съобщението е доставено"
|
||||||
},
|
},
|
||||||
"failed": {
|
"failed": {
|
||||||
"label": "Message delivery failed",
|
"label": "Доставката на съобщението не е успешна",
|
||||||
"displayText": "Delivery failed"
|
"displayText": "Неуспешна доставка"
|
||||||
},
|
},
|
||||||
"unknown": {
|
"unknown": {
|
||||||
"label": "Статусът на съобщението е неизвестен",
|
"label": "Статусът на съобщението е неизвестен",
|
||||||
"displayText": "Неизвестно състояние"
|
"displayText": "Неизвестно състояние"
|
||||||
},
|
},
|
||||||
"waiting": {
|
"waiting": {
|
||||||
"label": "Sending message",
|
"label": "Изпращане на съобщение",
|
||||||
"displayText": "Waiting for delivery"
|
"displayText": "Чака доставка"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,8 +45,8 @@
|
|||||||
"description": "Enable Codec 2 audio encoding"
|
"description": "Enable Codec 2 audio encoding"
|
||||||
},
|
},
|
||||||
"pttPin": {
|
"pttPin": {
|
||||||
"label": "PTT Pin",
|
"label": "Пин за РТТ",
|
||||||
"description": "GPIO pin to use for PTT"
|
"description": "GPIO пин, който да се използва за PTT"
|
||||||
},
|
},
|
||||||
"bitrate": {
|
"bitrate": {
|
||||||
"label": "Bitrate",
|
"label": "Bitrate",
|
||||||
|
|||||||
@@ -28,10 +28,10 @@
|
|||||||
"label": "Височина"
|
"label": "Височина"
|
||||||
},
|
},
|
||||||
"channelUtil": {
|
"channelUtil": {
|
||||||
"label": "Channel Util"
|
"label": "Използване на канала"
|
||||||
},
|
},
|
||||||
"airtimeUtil": {
|
"airtimeUtil": {
|
||||||
"label": "Airtime Util"
|
"label": "Използване на ефирно време"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"nodesTable": {
|
"nodesTable": {
|
||||||
|
|||||||
@@ -136,7 +136,7 @@
|
|||||||
"label": "Филтър"
|
"label": "Филтър"
|
||||||
},
|
},
|
||||||
"advanced": {
|
"advanced": {
|
||||||
"label": "Advanced"
|
"label": "Разширени"
|
||||||
},
|
},
|
||||||
"clearInput": {
|
"clearInput": {
|
||||||
"label": "Clear input"
|
"label": "Clear input"
|
||||||
@@ -149,7 +149,7 @@
|
|||||||
"placeholder": "Meshtastic 1234"
|
"placeholder": "Meshtastic 1234"
|
||||||
},
|
},
|
||||||
"airtimeUtilization": {
|
"airtimeUtilization": {
|
||||||
"label": "Airtime Utilization (%)"
|
"label": "Използване на ефира (%)"
|
||||||
},
|
},
|
||||||
"batteryLevel": {
|
"batteryLevel": {
|
||||||
"label": "Ниво на батерията (%)",
|
"label": "Ниво на батерията (%)",
|
||||||
@@ -179,16 +179,16 @@
|
|||||||
"label": "Любими"
|
"label": "Любими"
|
||||||
},
|
},
|
||||||
"hide": {
|
"hide": {
|
||||||
"label": "Hide"
|
"label": "Скриване"
|
||||||
},
|
},
|
||||||
"showOnly": {
|
"showOnly": {
|
||||||
"label": "Show Only"
|
"label": "Показване само"
|
||||||
},
|
},
|
||||||
"viaMqtt": {
|
"viaMqtt": {
|
||||||
"label": "Свързан чрез MQTT"
|
"label": "Свързан чрез MQTT"
|
||||||
},
|
},
|
||||||
"hopsUnknown": {
|
"hopsUnknown": {
|
||||||
"label": "Unknown number of hops"
|
"label": "Неизвестен брой хопове"
|
||||||
},
|
},
|
||||||
"showUnheard": {
|
"showUnheard": {
|
||||||
"label": "Never heard"
|
"label": "Never heard"
|
||||||
|
|||||||
@@ -33,9 +33,9 @@
|
|||||||
"qrGenerator": "Generator",
|
"qrGenerator": "Generator",
|
||||||
"qrImport": "Import",
|
"qrImport": "Import",
|
||||||
"scheduleShutdown": "Schedule Shutdown",
|
"scheduleShutdown": "Schedule Shutdown",
|
||||||
"scheduleReboot": "Schedule Reboot",
|
"scheduleReboot": "Reboot Device",
|
||||||
"rebootToOtaMode": "Reboot To OTA Mode",
|
|
||||||
"resetNodeDb": "Reset Node DB",
|
"resetNodeDb": "Reset Node DB",
|
||||||
|
"dfuMode": "Enter DFU Mode",
|
||||||
"factoryResetDevice": "Factory Reset Device",
|
"factoryResetDevice": "Factory Reset Device",
|
||||||
"factoryResetConfig": "Factory Reset Config",
|
"factoryResetConfig": "Factory Reset Config",
|
||||||
"disconnect": "Disconnect"
|
"disconnect": "Disconnect"
|
||||||
|
|||||||
@@ -17,7 +17,6 @@
|
|||||||
"now": "Now",
|
"now": "Now",
|
||||||
"ok": "OK",
|
"ok": "OK",
|
||||||
"print": "Print",
|
"print": "Print",
|
||||||
"rebootOtaNow": "Reboot to OTA Mode Now",
|
|
||||||
"remove": "Remove",
|
"remove": "Remove",
|
||||||
"requestNewKeys": "Request New Keys",
|
"requestNewKeys": "Request New Keys",
|
||||||
"requestPosition": "Request Position",
|
"requestPosition": "Request Position",
|
||||||
@@ -108,5 +107,7 @@
|
|||||||
"managed": "At least one admin key is requred if the node is managed.",
|
"managed": "At least one admin key is requred if the node is managed.",
|
||||||
"key": "Key is required."
|
"key": "Key is required."
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"yes": "Yes",
|
||||||
|
"no": "No"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,8 +76,9 @@
|
|||||||
"unknownErrorLoadingDevices": "Unknown error loading devices"
|
"unknownErrorLoadingDevices": "Unknown error loading devices"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"requiresFeatures": "This connection type requires <0></0>. Please use a supported browser, like Chrome or Edge.",
|
"requiresWebBluetooth": "This connection type requires <0>Web Bluetooth</0>. Please use a supported browser, like Chrome or Edge.",
|
||||||
"requiresSecureContext": "This application requires a <0>secure context</0>. Please connect using HTTPS or localhost.",
|
"requiresWebSerial": "This connection type requires <0>Web Serial</0>. Please use a supported browser, like Chrome or Edge.",
|
||||||
|
"requiresSecureContext": "This application requires a <0>secure context</0>. Please connect using HTTPS or localhost.",
|
||||||
"additionallyRequiresSecureContext": "Additionally, it requires a <0>secure context</0>. Please connect using HTTPS or localhost."
|
"additionallyRequiresSecureContext": "Additionally, it requires a <0>secure context</0>. Please connect using HTTPS or localhost."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -93,7 +94,7 @@
|
|||||||
"deviceMetrics": "Device Metrics:",
|
"deviceMetrics": "Device Metrics:",
|
||||||
"hardware": "Hardware: ",
|
"hardware": "Hardware: ",
|
||||||
"lastHeard": "Last Heard: ",
|
"lastHeard": "Last Heard: ",
|
||||||
"nodeHexPrefix": "Node Hex: !",
|
"nodeHexPrefix": "Node Hex: ",
|
||||||
"nodeNumber": "Node Number: ",
|
"nodeNumber": "Node Number: ",
|
||||||
"position": "Position:",
|
"position": "Position:",
|
||||||
"role": "Role: ",
|
"role": "Role: ",
|
||||||
@@ -102,7 +103,12 @@
|
|||||||
"title": "Node Details for {{identifier}}",
|
"title": "Node Details for {{identifier}}",
|
||||||
"ignoreNode": "Ignore node",
|
"ignoreNode": "Ignore node",
|
||||||
"removeNode": "Remove 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": {
|
"pkiBackup": {
|
||||||
"loseKeysWarning": "If you lose your keys, you will need to reset your device.",
|
"loseKeysWarning": "If you lose your keys, you will need to reset your device.",
|
||||||
@@ -132,15 +138,15 @@
|
|||||||
"sharableUrl": "Sharable URL",
|
"sharableUrl": "Sharable URL",
|
||||||
"title": "Generate QR Code"
|
"title": "Generate QR Code"
|
||||||
},
|
},
|
||||||
"rebootOta": {
|
|
||||||
"title": "Schedule Reboot",
|
|
||||||
"description": "Reboot the connected node after a delay into OTA (Over-the-Air) mode.",
|
|
||||||
"enterDelay": "Enter delay (sec)",
|
|
||||||
"scheduled": "Reboot has been scheduled"
|
|
||||||
},
|
|
||||||
"reboot": {
|
"reboot": {
|
||||||
"title": "Schedule Reboot",
|
"title": "Reboot device",
|
||||||
"description": "Reboot the connected node after x minutes."
|
"description": "Reboot now or schedule a reboot of the connected node. Optionally, you can choose to reboot into OTA (Over-the-Air) mode.",
|
||||||
|
"ota": "Reboot into OTA mode",
|
||||||
|
"enterDelay": "Enter delay",
|
||||||
|
"scheduled": "Reboot has been scheduled",
|
||||||
|
"schedule": "Schedule reboot",
|
||||||
|
"now": "Reboot now",
|
||||||
|
"cancel": "Cancel scheduled reboot"
|
||||||
},
|
},
|
||||||
"refreshKeys": {
|
"refreshKeys": {
|
||||||
"description": {
|
"description": {
|
||||||
@@ -179,5 +185,10 @@
|
|||||||
"confirmUnderstanding": "Yes, I know what I'm doing",
|
"confirmUnderstanding": "Yes, I know what I'm doing",
|
||||||
"title": "Are you sure?",
|
"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."
|
"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."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"label": "No Public Key"
|
"label": "No Public Key"
|
||||||
},
|
},
|
||||||
"directMessage": {
|
"directMessage": {
|
||||||
"label": "Direct Message {{shortName}}"
|
"label": "Direk Mesaj {{shortName}}"
|
||||||
},
|
},
|
||||||
"favorite": {
|
"favorite": {
|
||||||
"label": "Favori",
|
"label": "Favori",
|
||||||
@@ -50,12 +50,12 @@
|
|||||||
"viaMqtt": ", via MQTT"
|
"viaMqtt": ", via MQTT"
|
||||||
},
|
},
|
||||||
"lastHeardStatus": {
|
"lastHeardStatus": {
|
||||||
"never": "Never"
|
"never": "Hiçbir zaman"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"added": "Added",
|
"added": "Eklendi",
|
||||||
"removed": "Removed",
|
"removed": "Kaldırıldı",
|
||||||
"ignoreNode": "Ignore Node",
|
"ignoreNode": "Ignore Node",
|
||||||
"unignoreNode": "Unignore Node",
|
"unignoreNode": "Unignore Node",
|
||||||
"requestPosition": "Request Position"
|
"requestPosition": "Request Position"
|
||||||
|
|||||||
@@ -6,9 +6,7 @@ import { KeyBackupReminder } from "@components/KeyBackupReminder.tsx";
|
|||||||
import { ErrorPage } from "@components/UI/ErrorPage.tsx";
|
import { ErrorPage } from "@components/UI/ErrorPage.tsx";
|
||||||
import Footer from "@components/UI/Footer.tsx";
|
import Footer from "@components/UI/Footer.tsx";
|
||||||
import { useTheme } from "@core/hooks/useTheme.ts";
|
import { useTheme } from "@core/hooks/useTheme.ts";
|
||||||
import { useAppStore } from "@core/stores/appStore.ts";
|
import { SidebarProvider, useAppStore, useDeviceStore } from "@core/stores";
|
||||||
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
|
||||||
import { SidebarProvider } from "@core/stores/sidebarStore.tsx";
|
|
||||||
import { Dashboard } from "@pages/Dashboard/index.tsx";
|
import { Dashboard } from "@pages/Dashboard/index.tsx";
|
||||||
import { Outlet } from "@tanstack/react-router";
|
import { Outlet } from "@tanstack/react-router";
|
||||||
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
import { TanStackRouterDevtools } from "@tanstack/react-router-devtools";
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import type { Device } from "@core/stores/deviceStore.ts";
|
import { type Device, DeviceContext } from "@core/stores";
|
||||||
import { DeviceContext } from "@core/stores/deviceStore.ts";
|
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
export interface DeviceWrapperProps {
|
export interface DeviceWrapperProps {
|
||||||
|
|||||||
@@ -8,8 +8,7 @@ import {
|
|||||||
CommandList,
|
CommandList,
|
||||||
} from "@components/UI/Command.tsx";
|
} from "@components/UI/Command.tsx";
|
||||||
import { usePinnedItems } from "@core/hooks/usePinnedItems.ts";
|
import { usePinnedItems } from "@core/hooks/usePinnedItems.ts";
|
||||||
import { useAppStore } from "@core/stores/appStore.ts";
|
import { useAppStore, useDevice, useDeviceStore } from "@core/stores";
|
||||||
import { useDevice, useDeviceStore } from "@core/stores/deviceStore.ts";
|
|
||||||
import { cn } from "@core/utils/cn.ts";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
import { useCommandState } from "cmdk";
|
import { useCommandState } from "cmdk";
|
||||||
@@ -20,6 +19,7 @@ import {
|
|||||||
CloudOff,
|
CloudOff,
|
||||||
EraserIcon,
|
EraserIcon,
|
||||||
FactoryIcon,
|
FactoryIcon,
|
||||||
|
HardDriveUpload,
|
||||||
LayersIcon,
|
LayersIcon,
|
||||||
LinkIcon,
|
LinkIcon,
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
@@ -190,10 +190,10 @@ export const CommandPalette = () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: t("contextual.command.rebootToOtaMode"),
|
label: t("contextual.command.dfuMode"),
|
||||||
icon: RefreshCwIcon,
|
icon: HardDriveUpload,
|
||||||
action() {
|
action() {
|
||||||
setDialogOpen("rebootOTA", true);
|
connection?.enterDfuMode()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
+72
@@ -0,0 +1,72 @@
|
|||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogDescription,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@components/UI/Dialog.tsx";
|
||||||
|
import { useDevice } from "@core/stores";
|
||||||
|
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 (
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>{t("clientNotification.title")}</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
{t([
|
||||||
|
`clientNotification.${getClientNotification(0)?.message}`,
|
||||||
|
getClientNotification(0)?.message ?? "",
|
||||||
|
])}
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={open} onOpenChange={localOnOpenChange}>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogClose />
|
||||||
|
{dialogContent}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
+2
-2
@@ -2,9 +2,9 @@ import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/De
|
|||||||
import { fireEvent, render, screen } from "@testing-library/react";
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
// Ensure the path is correct for import
|
// Ensure the path is correct for import
|
||||||
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
|
import { useMessageStore } from "@core/stores";
|
||||||
|
|
||||||
vi.mock("@core/stores/messageStore", () => ({
|
vi.mock("@core/stores", () => ({
|
||||||
useMessageStore: vi.fn(() => ({
|
useMessageStore: vi.fn(() => ({
|
||||||
deleteAllMessages: vi.fn(),
|
deleteAllMessages: vi.fn(),
|
||||||
})),
|
})),
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@components/UI/Dialog.tsx";
|
} from "@components/UI/Dialog.tsx";
|
||||||
|
import { useMessageStore } from "@core/stores";
|
||||||
import { AlertTriangleIcon } from "lucide-react";
|
import { AlertTriangleIcon } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
|
|
||||||
|
|
||||||
export interface DeleteMessagesDialogProps {
|
export interface DeleteMessagesDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@components/UI/Dialog.tsx";
|
} from "@components/UI/Dialog.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { ClientNotificationDialog } from "@components/Dialog/ClientNotificationDialog/ClientNotificationDialog.tsx";
|
||||||
import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx";
|
import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx";
|
||||||
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.tsx";
|
import { DeviceNameDialog } from "@components/Dialog/DeviceNameDialog.tsx";
|
||||||
import { ImportDialog } from "@components/Dialog/ImportDialog.tsx";
|
import { ImportDialog } from "@components/Dialog/ImportDialog.tsx";
|
||||||
@@ -5,12 +6,11 @@ import { NodeDetailsDialog } from "@components/Dialog/NodeDetailsDialog/NodeDeta
|
|||||||
import { PkiBackupDialog } from "@components/Dialog/PKIBackupDialog.tsx";
|
import { PkiBackupDialog } from "@components/Dialog/PKIBackupDialog.tsx";
|
||||||
import { QRDialog } from "@components/Dialog/QRDialog.tsx";
|
import { QRDialog } from "@components/Dialog/QRDialog.tsx";
|
||||||
import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
|
import { RebootDialog } from "@components/Dialog/RebootDialog.tsx";
|
||||||
import { RebootOTADialog } from "@components/Dialog/RebootOTADialog.tsx";
|
|
||||||
import { RefreshKeysDialog } from "@components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx";
|
import { RefreshKeysDialog } from "@components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx";
|
||||||
import { RemoveNodeDialog } from "@components/Dialog/RemoveNodeDialog.tsx";
|
import { RemoveNodeDialog } from "@components/Dialog/RemoveNodeDialog.tsx";
|
||||||
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
|
import { ShutdownDialog } from "@components/Dialog/ShutdownDialog.tsx";
|
||||||
import { UnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx";
|
import { UnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/UnsafeRolesDialog.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
|
|
||||||
export const DialogManager = () => {
|
export const DialogManager = () => {
|
||||||
const { channels, config, dialog, setDialogOpen } = useDevice();
|
const { channels, config, dialog, setDialogOpen } = useDevice();
|
||||||
@@ -79,18 +79,18 @@ export const DialogManager = () => {
|
|||||||
setDialogOpen("refreshKeys", open);
|
setDialogOpen("refreshKeys", open);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<RebootOTADialog
|
|
||||||
open={dialog.rebootOTA}
|
|
||||||
onOpenChange={(open) => {
|
|
||||||
setDialogOpen("rebootOTA", open);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<DeleteMessagesDialog
|
<DeleteMessagesDialog
|
||||||
open={dialog.deleteMessages}
|
open={dialog.deleteMessages}
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
setDialogOpen("deleteMessages", open);
|
setDialogOpen("deleteMessages", open);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
<ClientNotificationDialog
|
||||||
|
open={dialog.clientNotification}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
setDialogOpen("clientNotification", open);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
import { Input } from "@components/UI/Input.tsx";
|
import { Input } from "@components/UI/Input.tsx";
|
||||||
import { Label } from "@components/UI/Label.tsx";
|
import { Label } from "@components/UI/Label.tsx";
|
||||||
import { Switch } from "@components/UI/Switch.tsx";
|
import { Switch } from "@components/UI/Switch.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { toByteArray } from "base64-js";
|
import { toByteArray } from "base64-js";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import type { Protobuf, Types } from "@meshtastic/core";
|
import type { Protobuf, Types } from "@meshtastic/core";
|
||||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -27,12 +27,12 @@ import {
|
|||||||
import { useFavoriteNode } from "@core/hooks/useFavoriteNode.ts";
|
import { useFavoriteNode } from "@core/hooks/useFavoriteNode.ts";
|
||||||
import { useIgnoreNode } from "@core/hooks/useIgnoreNode.ts";
|
import { useIgnoreNode } from "@core/hooks/useIgnoreNode.ts";
|
||||||
import { toast } from "@core/hooks/useToast.ts";
|
import { toast } from "@core/hooks/useToast.ts";
|
||||||
import { useAppStore } from "@core/stores/appStore.ts";
|
import { useAppStore, useDevice } from "@core/stores";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
|
||||||
import { cn } from "@core/utils/cn.ts";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
import { fromByteArray } from "base64-js";
|
||||||
import {
|
import {
|
||||||
BellIcon,
|
BellIcon,
|
||||||
BellOffIcon,
|
BellOffIcon,
|
||||||
@@ -167,7 +167,8 @@ export const NodeDetailsDialog = ({
|
|||||||
key: "batteryLevel",
|
key: "batteryLevel",
|
||||||
label: t("nodeDetails.batteryLevel"),
|
label: t("nodeDetails.batteryLevel"),
|
||||||
value: node.deviceMetrics?.batteryLevel,
|
value: node.deviceMetrics?.batteryLevel,
|
||||||
format: (val: number) => `${val.toFixed(2)}%`,
|
format: (val: number) =>
|
||||||
|
val === 101 ? t("batteryStatus.pluggedIn") : `${val.toFixed(2)}%`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: "voltage",
|
key: "voltage",
|
||||||
@@ -177,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 (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
<DialogContent aria-describedby={undefined}>
|
<DialogContent aria-describedby={undefined}>
|
||||||
@@ -192,7 +196,7 @@ export const NodeDetailsDialog = ({
|
|||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<div className="w-full">
|
<div className="w-full ">
|
||||||
<div className="flex flex-row flex-wrap space-y-1">
|
<div className="flex flex-row flex-wrap space-y-1">
|
||||||
<Button
|
<Button
|
||||||
className="mr-1"
|
className="mr-1"
|
||||||
@@ -270,79 +274,134 @@ export const NodeDetailsDialog = ({
|
|||||||
<p className="text-lg font-semibold">
|
<p className="text-lg font-semibold">
|
||||||
{t("nodeDetails.details")}
|
{t("nodeDetails.details")}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<table className="table-fixed w-full">
|
||||||
{t("nodeDetails.nodeNumber")}
|
<tbody>
|
||||||
{node.num}
|
<tr>
|
||||||
</p>
|
<td>{t("nodeDetails.nodeNumber")}</td>
|
||||||
<p>
|
<td>{node.num}</td>
|
||||||
{t("nodeDetails.nodeHexPrefix")}
|
</tr>
|
||||||
{numberToHexUnpadded(node.num)}
|
<tr>
|
||||||
</p>
|
<td>{t("nodeDetails.nodeHexPrefix")}</td>
|
||||||
<p>
|
<td>!{numberToHexUnpadded(node.num)}</td>
|
||||||
{t("nodeDetails.role")}
|
</tr>
|
||||||
{Protobuf.Config.Config_DeviceConfig_Role[
|
<tr>
|
||||||
node.user?.role ?? 0
|
<td>{t("nodeDetails.role")}</td>
|
||||||
].replace(/_/g, " ")}
|
<td>
|
||||||
</p>
|
{Protobuf.Config.Config_DeviceConfig_Role[
|
||||||
<p>
|
node.user?.role ?? 0
|
||||||
{t("nodeDetails.lastHeard")}
|
]?.replace(/_/g, " ")}
|
||||||
{node.lastHeard === 0 ? (
|
</td>
|
||||||
t("nodesTable.lastHeardStatus.never", { ns: "nodes" })
|
</tr>
|
||||||
) : (
|
<tr>
|
||||||
<TimeAgo timestamp={node.lastHeard * 1000} />
|
<td>{t("nodeDetails.lastHeard")}</td>
|
||||||
)}
|
<td>
|
||||||
</p>
|
{node.lastHeard === 0 ? (
|
||||||
<p>
|
t("nodesTable.lastHeardStatus.never", {
|
||||||
{t("nodeDetails.hardware")}
|
ns: "nodes",
|
||||||
{(
|
})
|
||||||
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ??
|
) : (
|
||||||
t("unknown.shortName")
|
<TimeAgo timestamp={node.lastHeard * 1000} />
|
||||||
).replace(/_/g, " ")}
|
)}
|
||||||
</p>
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{t("nodeDetails.hardware")}</td>
|
||||||
|
<td>
|
||||||
|
{(
|
||||||
|
Protobuf.Mesh.HardwareModel[
|
||||||
|
node.user?.hwModel ?? 0
|
||||||
|
] ?? t("unknown.shortName")
|
||||||
|
).replace(/_/g, " ")}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>{t("nodeDetails.messageable")}</td>
|
||||||
|
<td>
|
||||||
|
{node.user?.isUnmessagable ? t("no") : t("yes")}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<DeviceImage
|
<DeviceImage
|
||||||
className="h-45 w-45 p-2 rounded-lg border-4 border-slate-200 dark:border-slate-800"
|
className="w-40 p-2 rounded-lg border-4 border-slate-200 dark:border-slate-800"
|
||||||
deviceType={
|
deviceType={
|
||||||
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0]
|
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ??
|
||||||
|
"UNKNOWN"
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<div className="text-slate-900 dark:text-slate-100 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
<div className={sectionClassName}>
|
||||||
|
<p className="text-lg font-semibold">
|
||||||
|
{t("nodeDetails.security")}
|
||||||
|
</p>
|
||||||
|
<table className="table-auto w-full">
|
||||||
|
<tbody>
|
||||||
|
<tr>
|
||||||
|
<td className="pr-2">{t("nodeDetails.publicKey")}</td>
|
||||||
|
<td>
|
||||||
|
<pre className="text-xs pt-0.5">
|
||||||
|
{node.user?.publicKey &&
|
||||||
|
node.user?.publicKey.length > 0
|
||||||
|
? fromByteArray(node.user.publicKey)
|
||||||
|
: t("unknown.longName")}
|
||||||
|
</pre>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td></td>
|
||||||
|
<td>
|
||||||
|
{node.isKeyManuallyVerified
|
||||||
|
? t("nodeDetails.KeyManuallyVerifiedTrue")
|
||||||
|
: t("nodeDetails.KeyManuallyVerifiedFalse")}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={sectionClassName}>
|
||||||
<p className="text-lg font-semibold">
|
<p className="text-lg font-semibold">
|
||||||
{t("nodeDetails.position")}
|
{t("nodeDetails.position")}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{node.position ? (
|
{node.position ? (
|
||||||
<>
|
<table className="table-auto w-full">
|
||||||
{node.position.latitudeI && node.position.longitudeI && (
|
<tbody>
|
||||||
<p>
|
{node.position.latitudeI && node.position.longitudeI && (
|
||||||
{t("locationResponse.coordinates")}
|
<tr>
|
||||||
<a
|
<td>{t("locationResponse.coordinates")}</td>
|
||||||
className="text-blue-500 dark:text-blue-400"
|
<td>
|
||||||
href={`https://www.openstreetmap.org/?mlat=${
|
<a
|
||||||
node.position.latitudeI / 1e7
|
className="text-blue-500 dark:text-blue-400"
|
||||||
}&mlon=${node.position.longitudeI / 1e7}&layers=N`}
|
href={`https://www.openstreetmap.org/?mlat=${
|
||||||
target="_blank"
|
node.position.latitudeI / 1e7
|
||||||
rel="noreferrer"
|
}&mlon=${node.position.longitudeI / 1e7}&layers=N`}
|
||||||
>
|
target="_blank"
|
||||||
{node.position.latitudeI / 1e7},{" "}
|
rel="noreferrer"
|
||||||
{node.position.longitudeI / 1e7}
|
>
|
||||||
</a>
|
{node.position.latitudeI / 1e7},{" "}
|
||||||
</p>
|
{node.position.longitudeI / 1e7}
|
||||||
)}
|
</a>
|
||||||
{node.position.altitude && (
|
</td>
|
||||||
<p>
|
</tr>
|
||||||
{t("locationResponse.altitude")}
|
)}
|
||||||
{node.position.altitude}
|
{node.position.altitude && (
|
||||||
{t("unit.meter.one")}
|
<tr>
|
||||||
</p>
|
<td>{t("locationResponse.altitude")}</td>
|
||||||
)}
|
<td>
|
||||||
</>
|
{node.position.altitude}
|
||||||
|
{t("unit.meter.suffix")}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
) : (
|
) : (
|
||||||
<p>{t("unknown.shortName")}</p>
|
<p>{t("unknown.longName")}</p>
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
onClick={handleRequestPosition}
|
onClick={handleRequestPosition}
|
||||||
@@ -355,28 +414,37 @@ export const NodeDetailsDialog = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{node.deviceMetrics && (
|
{node.deviceMetrics && (
|
||||||
<div className="text-slate-900 dark:text-slate-100 bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
<div className={sectionClassName}>
|
||||||
<p className="text-lg font-semibold text-slate-900 dark:text-slate-100">
|
<p className="text-lg font-semibold text-slate-900 dark:text-slate-100">
|
||||||
{t("nodeDetails.deviceMetrics")}
|
{t("nodeDetails.deviceMetrics")}
|
||||||
</p>
|
</p>
|
||||||
{deviceMetricsMap
|
<table className="table-fixed w-full">
|
||||||
.filter((metric) => metric.value !== undefined)
|
<tbody>
|
||||||
.map((metric) => (
|
{deviceMetricsMap
|
||||||
<p key={metric.key}>
|
.filter((metric) => metric.value !== undefined)
|
||||||
{metric.label}: {metric.format(metric?.value ?? 0)}
|
.map((metric) => (
|
||||||
</p>
|
<tr key={metric.key}>
|
||||||
))}
|
<td>{metric.label}: </td>
|
||||||
{node.deviceMetrics.uptimeSeconds && (
|
<td>{metric.format(metric?.value ?? 0)}</td>
|
||||||
<p>
|
</tr>
|
||||||
{t("nodeDetails.uptime")}
|
))}
|
||||||
<Uptime seconds={node.deviceMetrics.uptimeSeconds} />
|
{node.deviceMetrics.uptimeSeconds && (
|
||||||
</p>
|
<tr>
|
||||||
)}
|
<td>{t("nodeDetails.uptime")}</td>
|
||||||
|
<td>
|
||||||
|
<Uptime
|
||||||
|
seconds={node.deviceMetrics.uptimeSeconds}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
)}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="text-slate-900 dark:text-slate-100 w-full max-w-[464px] bg-slate-100 dark:bg-slate-800 p-3 rounded-lg mt-3">
|
<div className="text-slate-900 dark:text-slate-100 w-full max-w-[464px] bg-slate-100 dark:bg-slate-800 rounded-lg mt-3">
|
||||||
<Accordion className="AccordionRoot" type="single" collapsible>
|
<Accordion className="AccordionRoot" type="single" collapsible>
|
||||||
<AccordionItem className="AccordionItem" value="item-1">
|
<AccordionItem className="AccordionItem" value="item-1">
|
||||||
<AccordionTrigger>
|
<AccordionTrigger>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import { fromByteArray } from "base64-js";
|
|||||||
import { DownloadIcon, PrinterIcon } from "lucide-react";
|
import { DownloadIcon, PrinterIcon } from "lucide-react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useDevice } from "../../core/stores/deviceStore.ts";
|
import { useDevice } from "../../core/stores";
|
||||||
import { Button } from "../UI/Button.tsx";
|
import { Button } from "../UI/Button.tsx";
|
||||||
|
|
||||||
export interface PkiBackupDialogProps {
|
export interface PkiBackupDialogProps {
|
||||||
|
|||||||
+68
-25
@@ -1,4 +1,4 @@
|
|||||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { fireEvent, render, screen } from "@testing-library/react";
|
||||||
import type {
|
import type {
|
||||||
ButtonHTMLAttributes,
|
ButtonHTMLAttributes,
|
||||||
ClassAttributes,
|
ClassAttributes,
|
||||||
@@ -7,14 +7,19 @@ import type {
|
|||||||
} from "react";
|
} from "react";
|
||||||
import type { JSX } from "react/jsx-runtime";
|
import type { JSX } from "react/jsx-runtime";
|
||||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { RebootOTADialog } from "./RebootOTADialog.tsx";
|
import { RebootDialog } from "./RebootDialog.tsx";
|
||||||
|
|
||||||
|
const rebootMock = vi.fn();
|
||||||
const rebootOtaMock = vi.fn();
|
const rebootOtaMock = vi.fn();
|
||||||
let mockConnection: { rebootOta: (delay: number) => void } | undefined = {
|
let mockConnection: {
|
||||||
|
rebootOta: (delay: number) => void,
|
||||||
|
reboot: (delay: number) => void
|
||||||
|
} | undefined = {
|
||||||
|
reboot: rebootMock,
|
||||||
rebootOta: rebootOtaMock,
|
rebootOta: rebootOtaMock,
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mock("@core/stores/deviceStore.ts", () => ({
|
vi.mock("@core/stores", () => ({
|
||||||
useDevice: () => ({
|
useDevice: () => ({
|
||||||
connection: mockConnection,
|
connection: mockConnection,
|
||||||
}),
|
}),
|
||||||
@@ -61,7 +66,7 @@ vi.mock("@components/UI/Dialog.tsx", () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("RebootOTADialog", () => {
|
describe("RebootDialog", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
rebootOtaMock.mockClear();
|
rebootOtaMock.mockClear();
|
||||||
@@ -72,17 +77,43 @@ describe("RebootOTADialog", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("renders dialog with default input value", () => {
|
it("renders dialog with default input value", () => {
|
||||||
render(<RebootOTADialog open onOpenChange={() => {}} />);
|
render(<RebootDialog open onOpenChange={() => {}} />);
|
||||||
expect(screen.getByPlaceholderText(/enter delay/i)).toHaveValue(5);
|
expect(screen.getByPlaceholderText(/enter delay/i)).toHaveValue(5);
|
||||||
expect(
|
expect(
|
||||||
screen.getByRole("heading", { name: /schedule reboot/i, level: 1 }),
|
screen.getByRole("heading", { name: /reboot device/i, level: 1 }),
|
||||||
).toBeInTheDocument();
|
).toBeInTheDocument();
|
||||||
expect(screen.getByText(/reboot to ota mode now/i)).toBeInTheDocument();
|
expect(screen.getByRole("button", { name: /reboot now/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("calls correct reboot function based on OTA checkbox state", () => {
|
||||||
|
render(<RebootDialog open onOpenChange={() => {}} />);
|
||||||
|
|
||||||
|
// Schedule non-OTA reboot
|
||||||
|
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
||||||
|
expect(rebootMock).toHaveBeenCalledWith(5);
|
||||||
|
expect(rebootOtaMock).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
rebootMock.mockClear();
|
||||||
|
rebootOtaMock.mockClear();
|
||||||
|
|
||||||
|
// Cancel scheduled
|
||||||
|
fireEvent.click(screen.getByTestId("cancelRebootBtn"));
|
||||||
|
expect(rebootMock).toHaveBeenCalledWith(-1);
|
||||||
|
expect(rebootOtaMock).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
rebootMock.mockClear();
|
||||||
|
rebootOtaMock.mockClear();
|
||||||
|
|
||||||
|
// Schedule OTA reboot
|
||||||
|
fireEvent.click(screen.getByText(/reboot into ota mode/i));
|
||||||
|
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
||||||
|
expect(rebootOtaMock).toHaveBeenCalledWith(5);
|
||||||
|
expect(rebootMock).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it("schedules a reboot with delay and calls rebootOta", async () => {
|
it("schedules a reboot with delay and calls rebootOta", async () => {
|
||||||
const onOpenChangeMock = vi.fn();
|
const onOpenChangeMock = vi.fn();
|
||||||
render(<RebootOTADialog open onOpenChange={onOpenChangeMock} />);
|
render(<RebootDialog open onOpenChange={onOpenChangeMock} />);
|
||||||
|
|
||||||
fireEvent.change(screen.getByPlaceholderText(/enter delay/i), {
|
fireEvent.change(screen.getByPlaceholderText(/enter delay/i), {
|
||||||
target: { value: "3" },
|
target: { value: "3" },
|
||||||
@@ -90,26 +121,24 @@ describe("RebootOTADialog", () => {
|
|||||||
|
|
||||||
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
||||||
|
|
||||||
|
expect(rebootMock).toHaveBeenCalledWith(3);
|
||||||
|
|
||||||
expect(screen.getByText(/reboot has been scheduled/i)).toBeInTheDocument();
|
expect(screen.getByText(/reboot has been scheduled/i)).toBeInTheDocument();
|
||||||
|
|
||||||
vi.advanceTimersByTime(3000);
|
vi.advanceTimersByTime(3000);
|
||||||
|
|
||||||
await waitFor(() => {
|
expect(onOpenChangeMock).toHaveBeenCalledWith(false);
|
||||||
expect(rebootOtaMock).toHaveBeenCalledWith(0);
|
|
||||||
expect(onOpenChangeMock).toHaveBeenCalledWith(false);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("triggers an instant reboot", async () => {
|
it("triggers an instant reboot", async () => {
|
||||||
const onOpenChangeMock = vi.fn();
|
const onOpenChangeMock = vi.fn();
|
||||||
render(<RebootOTADialog open onOpenChange={onOpenChangeMock} />);
|
render(<RebootDialog open onOpenChange={onOpenChangeMock} />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByText(/reboot to ota mode now/i));
|
fireEvent.click(screen.getByRole("button", { name: /reboot now/i }));
|
||||||
|
|
||||||
await waitFor(() => {
|
expect(rebootMock).toHaveBeenCalledWith(0);
|
||||||
expect(rebootOtaMock).toHaveBeenCalledWith(5);
|
expect(onOpenChangeMock).toHaveBeenCalledWith(false);
|
||||||
expect(onOpenChangeMock).toHaveBeenCalledWith(false);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("does not call reboot if connection is undefined", async () => {
|
it("does not call reboot if connection is undefined", async () => {
|
||||||
@@ -117,16 +146,30 @@ describe("RebootOTADialog", () => {
|
|||||||
|
|
||||||
mockConnection = undefined;
|
mockConnection = undefined;
|
||||||
|
|
||||||
render(<RebootOTADialog open onOpenChange={onOpenChangeMock} />);
|
render(<RebootDialog open onOpenChange={onOpenChangeMock} />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
||||||
|
|
||||||
vi.advanceTimersByTime(5000);
|
vi.advanceTimersByTime(5000);
|
||||||
|
|
||||||
await waitFor(() => {
|
expect(rebootMock).not.toHaveBeenCalled();
|
||||||
expect(rebootOtaMock).not.toHaveBeenCalled();
|
expect(rebootOtaMock).not.toHaveBeenCalled();
|
||||||
expect(onOpenChangeMock).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
mockConnection = { rebootOta: rebootOtaMock };
|
mockConnection = { reboot: rebootMock, rebootOta: rebootOtaMock };
|
||||||
|
});
|
||||||
|
|
||||||
|
it("cancels a scheduled reboot and calls rebootOta with -1", async () => {
|
||||||
|
const onOpenChangeMock = vi.fn();
|
||||||
|
render(<RebootDialog open onOpenChange={onOpenChangeMock} />);
|
||||||
|
|
||||||
|
fireEvent.change(screen.getByPlaceholderText(/enter delay/i), {
|
||||||
|
target: { value: "4" },
|
||||||
|
});
|
||||||
|
fireEvent.click(screen.getByTestId("scheduleRebootBtn"));
|
||||||
|
expect(rebootMock).toHaveBeenCalledWith(4);
|
||||||
|
|
||||||
|
fireEvent.click(screen.getByRole("button", { name: /cancel/i }));
|
||||||
|
expect(rebootMock).toHaveBeenCalledWith(-1);
|
||||||
|
expect(screen.queryByText(/reboot has been scheduled/i)).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import { Button } from "@components/UI/Button.tsx";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
|
import { Checkbox } from "@components/UI/Checkbox/index.tsx";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogClose,
|
DialogClose,
|
||||||
@@ -8,8 +9,10 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@components/UI/Dialog.tsx";
|
} from "@components/UI/Dialog.tsx";
|
||||||
import { Input } from "@components/UI/Input.tsx";
|
import { Input } from "@components/UI/Input.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { Label } from "@components/UI/Label.tsx";
|
||||||
import { RefreshCwIcon } from "lucide-react";
|
import { Separator } from "@components/UI/Seperator.tsx";
|
||||||
|
import { useDevice } from "@core/stores";
|
||||||
|
import { ClockIcon, OctagonXIcon, RefreshCwIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
@@ -18,11 +21,69 @@ export interface RebootDialogProps {
|
|||||||
onOpenChange: (open: boolean) => void;
|
onOpenChange: (open: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_REBOOT_DELAY = 5; // seconds
|
||||||
|
|
||||||
export const RebootDialog = ({ open, onOpenChange }: RebootDialogProps) => {
|
export const RebootDialog = ({ open, onOpenChange }: RebootDialogProps) => {
|
||||||
const { t } = useTranslation("dialog");
|
const { t } = useTranslation("dialog");
|
||||||
const { connection } = useDevice();
|
const { connection } = useDevice();
|
||||||
|
const [time, setTime] = useState<number>(DEFAULT_REBOOT_DELAY);
|
||||||
|
const [isScheduled, setIsScheduled] = useState(false);
|
||||||
|
const [isOTA, setIsOTA] = useState(false);
|
||||||
|
const [inputValue, setInputValue] = useState(DEFAULT_REBOOT_DELAY.toString());
|
||||||
|
const [timeoutId, setTimeoutId] = useState<NodeJS.Timeout | undefined>();
|
||||||
|
|
||||||
const [time, setTime] = useState<number>(5);
|
const handleReboot = (delay: number) => {
|
||||||
|
if (!connection) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOTA) {
|
||||||
|
connection.rebootOta(delay);
|
||||||
|
} else {
|
||||||
|
connection.reboot(delay);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSetTime = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (!e.target.validity.valid) {
|
||||||
|
e.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const val = e.target.value;
|
||||||
|
setInputValue(val);
|
||||||
|
|
||||||
|
const parsed = Number(val);
|
||||||
|
if (!Number.isNaN(parsed) && parsed > 0) {
|
||||||
|
setTime(parsed);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRebootWithTimeout = async () => {
|
||||||
|
setIsScheduled(true);
|
||||||
|
|
||||||
|
const delay = time > 0 ? time : DEFAULT_REBOOT_DELAY;
|
||||||
|
|
||||||
|
handleReboot(delay);
|
||||||
|
|
||||||
|
const id = setTimeout(() => {
|
||||||
|
setIsScheduled(false);
|
||||||
|
onOpenChange(false);
|
||||||
|
setInputValue(DEFAULT_REBOOT_DELAY.toString());
|
||||||
|
}, delay * 1000);
|
||||||
|
setTimeoutId(id);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
setIsScheduled(false);
|
||||||
|
handleReboot(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInstantReboot = async () => {
|
||||||
|
handleReboot(0);
|
||||||
|
onOpenChange(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||||
@@ -32,24 +93,66 @@ export const RebootDialog = ({ open, onOpenChange }: RebootDialogProps) => {
|
|||||||
<DialogTitle>{t("reboot.title")}</DialogTitle>
|
<DialogTitle>{t("reboot.title")}</DialogTitle>
|
||||||
<DialogDescription>{t("reboot.description")}</DialogDescription>
|
<DialogDescription>{t("reboot.description")}</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="flex gap-2 p-4">
|
<Separator />
|
||||||
<Input
|
{!isScheduled ? (
|
||||||
type="number"
|
<>
|
||||||
className="dark:text-slate-900"
|
<Checkbox
|
||||||
value={time}
|
checked={isOTA}
|
||||||
onChange={(e) => setTime(Number.parseInt(e.target.value))}
|
onChange={(checked) => setIsOTA(checked)}
|
||||||
/>
|
className="px-2"
|
||||||
<Button
|
>
|
||||||
className="w-24"
|
{t("reboot.ota")}
|
||||||
name="now"
|
</Checkbox>
|
||||||
onClick={() => {
|
<div className="flex gap-2 px-2 items-center relative">
|
||||||
connection?.reboot(2).then(() => onOpenChange(false));
|
<Input
|
||||||
}}
|
type="number"
|
||||||
>
|
min={1}
|
||||||
<RefreshCwIcon className="mr-2" size={16} />
|
max={86400}
|
||||||
{t("button.now")}
|
value={inputValue}
|
||||||
</Button>
|
onChange={handleSetTime}
|
||||||
</div>
|
placeholder={t("reboot.enterDelay")}
|
||||||
|
suffix={t("unit.second.plural")}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
onClick={() => handleRebootWithTimeout()}
|
||||||
|
data-testid="scheduleRebootBtn"
|
||||||
|
className="w-9/12"
|
||||||
|
>
|
||||||
|
<ClockIcon className="mr-2" size={18} />
|
||||||
|
{t("reboot.schedule")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div className="px-2">
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
name="rebootNow"
|
||||||
|
onClick={() => handleInstantReboot()}
|
||||||
|
className=" w-full"
|
||||||
|
>
|
||||||
|
<RefreshCwIcon className="mr-2" size={16} />
|
||||||
|
{t("reboot.now")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="px-2">
|
||||||
|
<div className="pb-6 pt-2 text-center">
|
||||||
|
<Label className=" text-gray-700 dark:text-gray-300 ">
|
||||||
|
{t("reboot.scheduled")}
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
variant="destructive"
|
||||||
|
name="cancelReboot"
|
||||||
|
onClick={() => handleCancel()}
|
||||||
|
className=" w-full"
|
||||||
|
data-testid="cancelRebootBtn"
|
||||||
|
>
|
||||||
|
<OctagonXIcon className="mr-2" size={16} />
|
||||||
|
{t("reboot.cancel")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
import { Button } from "@components/UI/Button.tsx";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogClose,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from "@components/UI/Dialog.tsx";
|
|
||||||
import { Input } from "@components/UI/Input.tsx";
|
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
|
||||||
import { ClockIcon, RefreshCwIcon } from "lucide-react";
|
|
||||||
import { useState } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
export interface RebootOTADialogProps {
|
|
||||||
open: boolean;
|
|
||||||
onOpenChange: (open: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DEFAULT_REBOOT_DELAY = 5; // seconds
|
|
||||||
|
|
||||||
export const RebootOTADialog = ({
|
|
||||||
open,
|
|
||||||
onOpenChange,
|
|
||||||
}: RebootOTADialogProps) => {
|
|
||||||
const { t } = useTranslation("dialog");
|
|
||||||
const { connection } = useDevice();
|
|
||||||
const [time, setTime] = useState<number>(DEFAULT_REBOOT_DELAY);
|
|
||||||
const [isScheduled, setIsScheduled] = useState(false);
|
|
||||||
const [inputValue, setInputValue] = useState(DEFAULT_REBOOT_DELAY.toString());
|
|
||||||
|
|
||||||
const handleSetTime = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
if (!e.target.validity.valid) {
|
|
||||||
e.preventDefault();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const val = e.target.value;
|
|
||||||
setInputValue(val);
|
|
||||||
|
|
||||||
const parsed = Number(val);
|
|
||||||
if (!Number.isNaN(parsed) && parsed > 0) {
|
|
||||||
setTime(parsed);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRebootWithTimeout = async () => {
|
|
||||||
if (!connection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setIsScheduled(true);
|
|
||||||
|
|
||||||
const delay = time > 0 ? time : DEFAULT_REBOOT_DELAY;
|
|
||||||
|
|
||||||
await new Promise<void>((resolve) => {
|
|
||||||
setTimeout(() => {
|
|
||||||
resolve();
|
|
||||||
}, delay * 1000);
|
|
||||||
}).finally(() => {
|
|
||||||
setIsScheduled(false);
|
|
||||||
onOpenChange(false);
|
|
||||||
setInputValue(DEFAULT_REBOOT_DELAY.toString());
|
|
||||||
});
|
|
||||||
connection.rebootOta(0);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInstantReboot = async () => {
|
|
||||||
if (!connection) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await connection.rebootOta(DEFAULT_REBOOT_DELAY);
|
|
||||||
onOpenChange(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogClose />
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>{t("rebootOta.title")}</DialogTitle>
|
|
||||||
<DialogDescription>{t("rebootOta.description")}</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
|
|
||||||
<div className="flex gap-2 p-2 items-center relative">
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
min={1}
|
|
||||||
max={86400}
|
|
||||||
className="dark:text-slate-900 appearance-none"
|
|
||||||
value={inputValue}
|
|
||||||
onChange={handleSetTime}
|
|
||||||
placeholder={t("rebootOta.enterDelay")}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
onClick={() => handleRebootWithTimeout()}
|
|
||||||
data-testid="scheduleRebootBtn"
|
|
||||||
className="w-9/12"
|
|
||||||
>
|
|
||||||
<ClockIcon className="mr-2" size={18} />
|
|
||||||
{isScheduled ? t("rebootOta.scheduled") : t("rebootOta.title")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
variant="destructive"
|
|
||||||
name="rebootNow"
|
|
||||||
onClick={() => handleInstantReboot()}
|
|
||||||
>
|
|
||||||
<RefreshCwIcon className="mr-2" size={16} />
|
|
||||||
{t("button.rebootOtaNow")}
|
|
||||||
</Button>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,11 +1,16 @@
|
|||||||
import { DeviceContext, useDeviceStore } from "@core/stores/deviceStore.ts";
|
import { DeviceContext, useDeviceStore, useMessageStore } from "@core/stores";
|
||||||
import { render } from "@testing-library/react";
|
import { render } from "@testing-library/react";
|
||||||
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
||||||
import { useMessageStore } from "../../../core/stores/messageStore/index.ts";
|
|
||||||
import { RefreshKeysDialog } from "./RefreshKeysDialog.tsx";
|
import { RefreshKeysDialog } from "./RefreshKeysDialog.tsx";
|
||||||
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
||||||
|
|
||||||
vi.mock("@core/stores/messageStore");
|
vi.mock("@core/stores", async () => {
|
||||||
|
const actual = (await vi.importActual("@core/stores")) as typeof import("@core/stores");
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useMessageStore: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
vi.mock("./useRefreshKeysDialog");
|
vi.mock("./useRefreshKeysDialog");
|
||||||
|
|
||||||
const mockUseMessageStore = vi.mocked(useMessageStore);
|
const mockUseMessageStore = vi.mocked(useMessageStore);
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import {
|
|||||||
DialogHeader,
|
DialogHeader,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@components/UI/Dialog.tsx";
|
} from "@components/UI/Dialog.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice, useMessageStore } from "@core/stores";
|
||||||
import { useMessageStore } from "@core/stores/messageStore/index.ts";
|
|
||||||
import { LockKeyholeOpenIcon } from "lucide-react";
|
import { LockKeyholeOpenIcon } from "lucide-react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice, useMessageStore } from "@core/stores";
|
||||||
import { useMessageStore } from "@core/stores/messageStore/index.ts";
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
export function useRefreshKeysDialog() {
|
export function useRefreshKeysDialog() {
|
||||||
|
|||||||
@@ -9,9 +9,8 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@components/UI/Dialog.tsx";
|
} from "@components/UI/Dialog.tsx";
|
||||||
import { Label } from "@components/UI/Label.tsx";
|
import { Label } from "@components/UI/Label.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useAppStore, useDevice } from "@core/stores";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useAppStore } from "../../core/stores/appStore.ts";
|
|
||||||
|
|
||||||
export interface RemoveNodeDialogProps {
|
export interface RemoveNodeDialogProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@components/UI/Dialog.tsx";
|
} from "@components/UI/Dialog.tsx";
|
||||||
import { Input } from "@components/UI/Input.tsx";
|
import { Input } from "@components/UI/Input.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { ClockIcon, PowerIcon } from "lucide-react";
|
import { ClockIcon, PowerIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import type { Protobuf, Types } from "@meshtastic/core";
|
import type { Protobuf, Types } from "@meshtastic/core";
|
||||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
} from "@components/UI/Dialog.tsx";
|
} from "@components/UI/Dialog.tsx";
|
||||||
import { Link } from "@components/UI/Typography/Link.tsx";
|
import { Link } from "@components/UI/Typography/Link.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { eventBus } from "@core/utils/eventBus.ts";
|
import { eventBus } from "@core/utils/eventBus.ts";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { eventBus } from "@core/utils/eventBus.ts";
|
import { eventBus } from "@core/utils/eventBus.ts";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { FieldWrapper } from "@components/Form/FormWrapper.tsx";
|
|||||||
import { Button } from "@components/UI/Button.tsx";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { Heading } from "@components/UI/Typography/Heading.tsx";
|
import { Heading } from "@components/UI/Typography/Heading.tsx";
|
||||||
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
||||||
import { useAppStore } from "@core/stores/appStore.ts";
|
import { useAppStore } from "@core/stores";
|
||||||
import { dotPaths } from "@core/utils/dotPath.ts";
|
import { dotPaths } from "@core/utils/dotPath.ts";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useBackupReminder } from "@core/hooks/useKeyBackupReminder.tsx";
|
import { useBackupReminder } from "@core/hooks/useKeyBackupReminder.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
export const KeyBackupReminder = () => {
|
export const KeyBackupReminder = () => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { create } from "@bufbuild/protobuf";
|
|||||||
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
|
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
|
||||||
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
import { DynamicForm } from "@components/Form/DynamicForm.tsx";
|
||||||
import { useToast } from "@core/hooks/useToast.ts";
|
import { useToast } from "@core/hooks/useToast.ts";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { fromByteArray, toByteArray } from "base64-js";
|
import { fromByteArray, toByteArray } from "base64-js";
|
||||||
import cryptoRandomString from "crypto-random-string";
|
import cryptoRandomString from "crypto-random-string";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import {
|
import {
|
||||||
convertIntToIpAddress,
|
convertIntToIpAddress,
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import {
|
|||||||
type FlagName,
|
type FlagName,
|
||||||
usePositionFlags,
|
usePositionFlags,
|
||||||
} from "@core/hooks/usePositionFlags.ts";
|
} from "@core/hooks/usePositionFlags.ts";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import {
|
|||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useAppStore } from "@core/stores/appStore.ts";
|
import { useAppStore, useDevice } from "@core/stores";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { getX25519PrivateKey, getX25519PublicKey } from "@core/utils/x25519.ts";
|
import { getX25519PrivateKey, getX25519PublicKey } from "@core/utils/x25519.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
@@ -197,14 +196,12 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
|
|||||||
description: t("security.primaryAdminKey.description"),
|
description: t("security.primaryAdminKey.description"),
|
||||||
bits,
|
bits,
|
||||||
devicePSKBitCount: 32,
|
devicePSKBitCount: 32,
|
||||||
hide: true,
|
|
||||||
actionButtons: [],
|
actionButtons: [],
|
||||||
disabledBy: [
|
disabledBy: [
|
||||||
{ fieldName: "adminChannelEnabled", invert: true },
|
{ fieldName: "adminChannelEnabled", invert: true },
|
||||||
],
|
],
|
||||||
properties: {
|
properties: {
|
||||||
showCopyButton: true,
|
showCopyButton: true,
|
||||||
showPasswordToggle: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -215,14 +212,12 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
|
|||||||
description: t("security.secondaryAdminKey.description"),
|
description: t("security.secondaryAdminKey.description"),
|
||||||
bits,
|
bits,
|
||||||
devicePSKBitCount: 32,
|
devicePSKBitCount: 32,
|
||||||
hide: true,
|
|
||||||
actionButtons: [],
|
actionButtons: [],
|
||||||
disabledBy: [
|
disabledBy: [
|
||||||
{ fieldName: "adminChannelEnabled", invert: true },
|
{ fieldName: "adminChannelEnabled", invert: true },
|
||||||
],
|
],
|
||||||
properties: {
|
properties: {
|
||||||
showCopyButton: true,
|
showCopyButton: true,
|
||||||
showPasswordToggle: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -233,14 +228,12 @@ export const Security = ({ onFormInit }: SecurityConfigProps) => {
|
|||||||
description: t("security.tertiaryAdminKey.description"),
|
description: t("security.tertiaryAdminKey.description"),
|
||||||
bits,
|
bits,
|
||||||
devicePSKBitCount: 32,
|
devicePSKBitCount: 32,
|
||||||
hide: true,
|
|
||||||
actionButtons: [],
|
actionButtons: [],
|
||||||
disabledBy: [
|
disabledBy: [
|
||||||
{ fieldName: "adminChannelEnabled", invert: true },
|
{ fieldName: "adminChannelEnabled", invert: true },
|
||||||
],
|
],
|
||||||
properties: {
|
properties: {
|
||||||
showCopyButton: true,
|
showCopyButton: true,
|
||||||
showPasswordToggle: true,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Mono } from "@components/generic/Mono.tsx";
|
import { Mono } from "@components/generic/Mono.tsx";
|
||||||
import { Button } from "@components/UI/Button.tsx";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { useAppStore } from "@core/stores/appStore.ts";
|
import { useAppStore, useDeviceStore, useMessageStore } from "@core/stores";
|
||||||
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
|
||||||
import { useMessageStore } from "@core/stores/messageStore/index.ts";
|
|
||||||
import { subscribeAll } from "@core/subscriptions.ts";
|
import { subscribeAll } from "@core/subscriptions.ts";
|
||||||
import { randId } from "@core/utils/randId.ts";
|
import { randId } from "@core/utils/randId.ts";
|
||||||
import { MeshDevice } from "@meshtastic/core";
|
import { MeshDevice } from "@meshtastic/core";
|
||||||
|
|||||||
@@ -4,14 +4,12 @@ import { TransportHTTP } from "@meshtastic/transport-http";
|
|||||||
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { describe, expect, it, vi } from "vitest";
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
vi.mock("@core/stores/appStore.ts", () => ({
|
vi.mock("@core/stores", () => ({
|
||||||
useAppStore: vi.fn(() => ({ setSelectedDevice: vi.fn() })),
|
useAppStore: vi.fn(() => ({ setSelectedDevice: vi.fn() })),
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@core/stores/deviceStore.ts", () => ({
|
|
||||||
useDeviceStore: vi.fn(() => ({
|
useDeviceStore: vi.fn(() => ({
|
||||||
addDevice: vi.fn(() => ({ addConnection: vi.fn() })),
|
addDevice: vi.fn(() => ({ addConnection: vi.fn() })),
|
||||||
})),
|
})),
|
||||||
|
useMessageStore: vi.fn()
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@core/utils/randId.ts", () => ({
|
vi.mock("@core/utils/randId.ts", () => ({
|
||||||
|
|||||||
@@ -4,9 +4,7 @@ import { Input } from "@components/UI/Input.tsx";
|
|||||||
import { Label } from "@components/UI/Label.tsx";
|
import { Label } from "@components/UI/Label.tsx";
|
||||||
import { Switch } from "@components/UI/Switch.tsx";
|
import { Switch } from "@components/UI/Switch.tsx";
|
||||||
import { Link } from "@components/UI/Typography/Link.tsx";
|
import { Link } from "@components/UI/Typography/Link.tsx";
|
||||||
import { useAppStore } from "@core/stores/appStore.ts";
|
import { useAppStore, useDeviceStore, useMessageStore } from "@core/stores";
|
||||||
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
|
||||||
import { useMessageStore } from "@core/stores/messageStore/index.ts";
|
|
||||||
import { subscribeAll } from "@core/subscriptions.ts";
|
import { subscribeAll } from "@core/subscriptions.ts";
|
||||||
import { randId } from "@core/utils/randId.ts";
|
import { randId } from "@core/utils/randId.ts";
|
||||||
import { MeshDevice } from "@meshtastic/core";
|
import { MeshDevice } from "@meshtastic/core";
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import { Mono } from "@components/generic/Mono.tsx";
|
import { Mono } from "@components/generic/Mono.tsx";
|
||||||
import { Button } from "@components/UI/Button.tsx";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { useAppStore } from "@core/stores/appStore.ts";
|
import { useAppStore, useDeviceStore, useMessageStore } from "@core/stores";
|
||||||
import { useDeviceStore } from "@core/stores/deviceStore.ts";
|
|
||||||
import { useMessageStore } from "@core/stores/messageStore/index.ts";
|
|
||||||
import { subscribeAll } from "@core/subscriptions.ts";
|
import { subscribeAll } from "@core/subscriptions.ts";
|
||||||
import { randId } from "@core/utils/randId.ts";
|
import { randId } from "@core/utils/randId.ts";
|
||||||
import { MeshDevice } from "@meshtastic/core";
|
import { MeshDevice } from "@meshtastic/core";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Button } from "@components/UI/Button.tsx";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { Input } from "@components/UI/Input.tsx";
|
import { Input } from "@components/UI/Input.tsx";
|
||||||
import { useMessageStore } from "@core/stores/messageStore/index.ts";
|
import { useMessageStore } from "@core/stores";
|
||||||
import type { Types } from "@meshtastic/core";
|
import type { Types } from "@meshtastic/core";
|
||||||
import { SendIcon } from "lucide-react";
|
import { SendIcon } from "lucide-react";
|
||||||
import { startTransition, useState } from "react";
|
import { startTransition, useState } from "react";
|
||||||
|
|||||||
@@ -6,11 +6,7 @@ import {
|
|||||||
TooltipProvider,
|
TooltipProvider,
|
||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@components/UI/Tooltip.tsx";
|
} from "@components/UI/Tooltip.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { MessageState, useDevice, useMessageStore } from "@core/stores";
|
||||||
import {
|
|
||||||
MessageState,
|
|
||||||
useMessageStore,
|
|
||||||
} from "@core/stores/messageStore/index.ts";
|
|
||||||
import type { Message } from "@core/stores/messageStore/types.ts";
|
import type { Message } from "@core/stores/messageStore/types.ts";
|
||||||
import { cn } from "@core/utils/cn.ts";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
import { type Protobuf, Types } from "@meshtastic/core";
|
import { type Protobuf, Types } from "@meshtastic/core";
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { TraceRoute } from "@components/PageComponents/Messages/TraceRoute.tsx";
|
import { TraceRoute } from "@components/PageComponents/Messages/TraceRoute.tsx";
|
||||||
import { mockDeviceStore } from "@core/stores/deviceStore.mock.ts";
|
import { mockDeviceStore } from "@core/stores/deviceStore/deviceStore.mock.ts";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { render, screen } from "@testing-library/react";
|
import { render, screen } from "@testing-library/react";
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
vi.mock("@core/stores/deviceStore");
|
vi.mock("@core/stores");
|
||||||
|
|
||||||
describe("TraceRoute", () => {
|
describe("TraceRoute", () => {
|
||||||
const fromUser = {
|
const fromUser = {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import type { Protobuf } from "@meshtastic/core";
|
import type { Protobuf } from "@meshtastic/core";
|
||||||
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
import { numberToHexUnpadded } from "@noble/curves/abstract/utils";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
DynamicForm,
|
DynamicForm,
|
||||||
type DynamicFormFormInit,
|
type DynamicFormFormInit,
|
||||||
} from "@components/Form/DynamicForm.tsx";
|
} from "@components/Form/DynamicForm.tsx";
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
import { useDevice } from "@core/stores";
|
||||||
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts";
|
||||||
import { Protobuf } from "@meshtastic/core";
|
import { Protobuf } from "@meshtastic/core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|||||||
@@ -2,10 +2,7 @@ import { SidebarButton } from "@components/UI/Sidebar/SidebarButton.tsx";
|
|||||||
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
|
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
|
||||||
import { Spinner } from "@components/UI/Spinner.tsx";
|
import { Spinner } from "@components/UI/Spinner.tsx";
|
||||||
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
||||||
import { useAppStore } from "@core/stores/appStore.ts";
|
import { type Page, useAppStore, useDevice, useSidebar } from "@core/stores";
|
||||||
import type { Page } from "@core/stores/deviceStore.ts";
|
|
||||||
import { useDevice } from "@core/stores/deviceStore.ts";
|
|
||||||
import { useSidebar } from "@core/stores/sidebarStore.tsx";
|
|
||||||
import { cn } from "@core/utils/cn.ts";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
import { useLocation, useNavigate } from "@tanstack/react-router";
|
import { useLocation, useNavigate } from "@tanstack/react-router";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,56 +1,75 @@
|
|||||||
import { Checkbox } from "@components/UI/Checkbox/index.tsx";
|
import { Checkbox } from "@components/UI/Checkbox/index.tsx";
|
||||||
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
import { cleanup, fireEvent, render, screen } from "@testing-library/react";
|
||||||
import type React from "react";
|
|
||||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
vi.mock("@components/UI/Label.tsx", () => ({
|
|
||||||
Label: ({
|
|
||||||
children,
|
|
||||||
className,
|
|
||||||
htmlFor,
|
|
||||||
id,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
className: string;
|
|
||||||
htmlFor: string;
|
|
||||||
id: string;
|
|
||||||
}) => (
|
|
||||||
<label
|
|
||||||
data-testid="label-component"
|
|
||||||
className={className}
|
|
||||||
htmlFor={htmlFor}
|
|
||||||
id={id}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</label>
|
|
||||||
),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("Checkbox", () => {
|
describe("Checkbox", () => {
|
||||||
beforeEach(cleanup);
|
beforeEach(cleanup);
|
||||||
|
|
||||||
it("renders unchecked by default", () => {
|
it("renders unchecked by default (uncontrolled)", () => {
|
||||||
render(<Checkbox />);
|
render(<Checkbox />);
|
||||||
const checkbox = screen.getByRole("checkbox");
|
const checkbox = screen.getByRole("checkbox");
|
||||||
|
const presentation = screen.getByRole("presentation");
|
||||||
expect(checkbox).not.toBeChecked();
|
expect(checkbox).not.toBeChecked();
|
||||||
expect(screen.queryByText("Check")).not.toBeInTheDocument();
|
// unchecked -> no filled bg class
|
||||||
|
expect(presentation).not.toHaveClass("bg-slate-500");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders checked when checked prop is true", () => {
|
it("respects defaultChecked in uncontrolled mode", () => {
|
||||||
render(<Checkbox checked />);
|
render(<Checkbox defaultChecked />);
|
||||||
expect(screen.getByRole("checkbox")).toBeChecked();
|
expect(screen.getByRole("checkbox")).toBeChecked();
|
||||||
expect(screen.getByRole("presentation")).toBeInTheDocument();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("calls onChange when clicked", () => {
|
it("renders checked when controlled with checked=true", () => {
|
||||||
|
render(<Checkbox checked />);
|
||||||
|
const checkbox = screen.getByRole("checkbox");
|
||||||
|
const presentation = screen.getByRole("presentation");
|
||||||
|
expect(checkbox).toBeChecked();
|
||||||
|
expect(presentation).toHaveClass("bg-slate-500");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("calls onChange when clicked (uncontrolled) and toggles DOM state", () => {
|
||||||
const onChange = vi.fn();
|
const onChange = vi.fn();
|
||||||
render(<Checkbox onChange={onChange} />);
|
render(<Checkbox onChange={onChange} />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("presentation"));
|
const checkbox = screen.getByRole("checkbox");
|
||||||
expect(onChange).toHaveBeenCalledWith(true);
|
const presentation = screen.getByRole("presentation");
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("presentation"));
|
fireEvent.click(presentation);
|
||||||
expect(onChange).toHaveBeenCalledWith(false);
|
expect(onChange).toHaveBeenLastCalledWith(true);
|
||||||
|
expect(checkbox).toBeChecked();
|
||||||
|
|
||||||
|
fireEvent.click(presentation);
|
||||||
|
expect(onChange).toHaveBeenLastCalledWith(false);
|
||||||
|
expect(checkbox).not.toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("controlled: calls onChange but does not toggle without prop update", () => {
|
||||||
|
const onChange = vi.fn();
|
||||||
|
render(<Checkbox checked={false} onChange={onChange} />);
|
||||||
|
|
||||||
|
const checkbox = screen.getByRole("checkbox");
|
||||||
|
const presentation = screen.getByRole("presentation");
|
||||||
|
|
||||||
|
fireEvent.click(presentation);
|
||||||
|
expect(onChange).toHaveBeenLastCalledWith(true);
|
||||||
|
// still unchecked because parent didn't update prop
|
||||||
|
expect(checkbox).not.toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("controlled: reflects external prop changes after onChange", () => {
|
||||||
|
const onChange = vi.fn();
|
||||||
|
const { rerender } = render(<Checkbox checked={false} onChange={onChange} />);
|
||||||
|
|
||||||
|
const checkbox = screen.getByRole("checkbox");
|
||||||
|
const presentation = screen.getByRole("presentation");
|
||||||
|
|
||||||
|
fireEvent.click(presentation);
|
||||||
|
expect(onChange).toHaveBeenLastCalledWith(true);
|
||||||
|
|
||||||
|
// parent updates `checked` based on onChange
|
||||||
|
rerender(<Checkbox checked={true} onChange={onChange} />);
|
||||||
|
expect(checkbox).toBeChecked();
|
||||||
|
expect(presentation).toHaveClass("bg-slate-500");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("uses provided id", () => {
|
it("uses provided id", () => {
|
||||||
@@ -58,23 +77,16 @@ describe("Checkbox", () => {
|
|||||||
expect(screen.getByRole("checkbox").id).toBe("custom-id");
|
expect(screen.getByRole("checkbox").id).toBe("custom-id");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders children in Label component", () => {
|
it("renders children inside the label", () => {
|
||||||
render(<Checkbox>Test Label</Checkbox>);
|
render(<Checkbox>Test Label</Checkbox>);
|
||||||
expect(screen.getByTestId("label-component")).toHaveTextContent(
|
expect(screen.getByTestId("label-component")).toHaveTextContent("Test Label");
|
||||||
"Test Label",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies custom className", () => {
|
it("applies custom className to wrapper label", () => {
|
||||||
const { container } = render(<Checkbox className="custom-class" />);
|
const { container } = render(<Checkbox className="custom-class" />);
|
||||||
expect(container.firstChild).toHaveClass("custom-class");
|
expect(container.firstChild).toHaveClass("custom-class");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("applies labelClassName to Label", () => {
|
|
||||||
render(<Checkbox labelClassName="label-class">Test</Checkbox>);
|
|
||||||
expect(screen.getByTestId("label-component")).toHaveClass("label-class");
|
|
||||||
});
|
|
||||||
|
|
||||||
it("disables checkbox when disabled prop is true", () => {
|
it("disables checkbox when disabled prop is true", () => {
|
||||||
render(<Checkbox disabled />);
|
render(<Checkbox disabled />);
|
||||||
expect(screen.getByRole("checkbox")).toBeDisabled();
|
expect(screen.getByRole("checkbox")).toBeDisabled();
|
||||||
@@ -84,7 +96,6 @@ describe("Checkbox", () => {
|
|||||||
it("does not call onChange when disabled", () => {
|
it("does not call onChange when disabled", () => {
|
||||||
const onChange = vi.fn();
|
const onChange = vi.fn();
|
||||||
render(<Checkbox onChange={onChange} disabled />);
|
render(<Checkbox onChange={onChange} disabled />);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("presentation"));
|
fireEvent.click(screen.getByRole("presentation"));
|
||||||
expect(onChange).not.toHaveBeenCalled();
|
expect(onChange).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -99,24 +110,19 @@ describe("Checkbox", () => {
|
|||||||
expect(screen.getByRole("checkbox")).toHaveAttribute("name", "test-name");
|
expect(screen.getByRole("checkbox")).toHaveAttribute("name", "test-name");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("passes through additional props", () => {
|
it("passes through additional props to the input", () => {
|
||||||
render(<Checkbox data-testid="extra-prop" />);
|
render(<Checkbox data-testid="extra-prop" />);
|
||||||
expect(screen.getByRole("checkbox")).toHaveAttribute(
|
expect(screen.getByRole("checkbox")).toHaveAttribute("data-testid", "extra-prop");
|
||||||
"data-testid",
|
|
||||||
"extra-prop",
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("toggles checked state correctly", () => {
|
it("uncontrolled: toggles checked state when clicking the visual box", () => {
|
||||||
render(<Checkbox />);
|
render(<Checkbox />);
|
||||||
const checkbox = screen.getByRole("checkbox");
|
const checkbox = screen.getByRole("checkbox");
|
||||||
const presentation = screen.getByRole("presentation");
|
const presentation = screen.getByRole("presentation");
|
||||||
|
|
||||||
expect(checkbox).not.toBeChecked();
|
expect(checkbox).not.toBeChecked();
|
||||||
|
|
||||||
fireEvent.click(presentation);
|
fireEvent.click(presentation);
|
||||||
expect(checkbox).toBeChecked();
|
expect(checkbox).toBeChecked();
|
||||||
|
|
||||||
fireEvent.click(presentation);
|
fireEvent.click(presentation);
|
||||||
expect(checkbox).not.toBeChecked();
|
expect(checkbox).not.toBeChecked();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { cn } from "@core/utils/cn.ts";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
import { Check } from "lucide-react";
|
import { Check } from "lucide-react";
|
||||||
import { useId } from "react";
|
import { useId, useState } from "react";
|
||||||
|
|
||||||
interface CheckboxProps {
|
interface CheckboxProps {
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
|
defaultChecked?: boolean;
|
||||||
onChange?: (checked: boolean) => void;
|
onChange?: (checked: boolean) => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
labelClassName?: string;
|
labelClassName?: string;
|
||||||
@@ -15,7 +16,8 @@ interface CheckboxProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Checkbox({
|
export function Checkbox({
|
||||||
checked = false,
|
checked,
|
||||||
|
defaultChecked = false,
|
||||||
onChange,
|
onChange,
|
||||||
className,
|
className,
|
||||||
id: propId,
|
id: propId,
|
||||||
@@ -28,10 +30,21 @@ export function Checkbox({
|
|||||||
const generatedId = useId();
|
const generatedId = useId();
|
||||||
const id = propId || generatedId;
|
const id = propId || generatedId;
|
||||||
|
|
||||||
const handleToggle = (): void => {
|
const isControlled = checked !== undefined;
|
||||||
if (!disabled) {
|
const [internal, setInternal] = useState<boolean>(defaultChecked);
|
||||||
onChange?.(!checked);
|
const value = checked ?? internal;
|
||||||
|
|
||||||
|
const handleToggle = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
if (disabled) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const next = e.target.checked;
|
||||||
|
|
||||||
|
if (!isControlled) {
|
||||||
|
setInternal(next);
|
||||||
|
}
|
||||||
|
onChange?.(next);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -41,11 +54,12 @@ export function Checkbox({
|
|||||||
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
|
disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
|
||||||
className,
|
className,
|
||||||
)}
|
)}
|
||||||
|
data-testid="label-component"
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id={id}
|
id={id}
|
||||||
checked={checked}
|
checked={value}
|
||||||
onChange={handleToggle}
|
onChange={handleToggle}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
required={required}
|
required={required}
|
||||||
@@ -57,10 +71,12 @@ export function Checkbox({
|
|||||||
className={cn(
|
className={cn(
|
||||||
"flex h-6 w-6 shrink-0 items-center justify-center rounded-md border-2 border-gray-500 transition-colors",
|
"flex h-6 w-6 shrink-0 items-center justify-center rounded-md border-2 border-gray-500 transition-colors",
|
||||||
"peer-focus-visible:ring-2 peer-focus-visible:ring-blue-500 peer-focus-visible:ring-offset-2",
|
"peer-focus-visible:ring-2 peer-focus-visible:ring-blue-500 peer-focus-visible:ring-offset-2",
|
||||||
{ "border-slate-500 bg-slate-500": checked },
|
{ "border-slate-500 bg-slate-500": value },
|
||||||
|
{ "opacity-50": disabled },
|
||||||
)}
|
)}
|
||||||
|
role="presentation"
|
||||||
>
|
>
|
||||||
{checked && (
|
{value && (
|
||||||
<div className="animate-fade-in">
|
<div className="animate-fade-in">
|
||||||
<Check className="h-4 w-4 text-white" />
|
<Check className="h-4 w-4 text-white" />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Button } from "@components/UI/Button.tsx";
|
import { Button } from "@components/UI/Button.tsx";
|
||||||
import { useSidebar } from "@core/stores/sidebarStore.tsx";
|
import { useSidebar } from "@core/stores";
|
||||||
import { cn } from "@core/utils/cn.ts";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
import type { LucideIcon } from "lucide-react";
|
import type { LucideIcon } from "lucide-react";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Heading } from "@components/UI/Typography/Heading.tsx";
|
import { Heading } from "@components/UI/Typography/Heading.tsx";
|
||||||
import { useSidebar } from "@core/stores/sidebarStore.tsx";
|
import { useSidebar } from "@core/stores";
|
||||||
import { cn } from "@core/utils/cn.ts";
|
import { cn } from "@core/utils/cn.ts";
|
||||||
import type React from "react";
|
import type React from "react";
|
||||||
|
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ export const FilterMulti = <K extends EnumArrayKeys<FilterState>>({
|
|||||||
key={val}
|
key={val}
|
||||||
checked={selected.includes(val)}
|
checked={selected.includes(val)}
|
||||||
onChange={(checked) => toggleValue(val, checked)}
|
onChange={(checked) => toggleValue(val, checked)}
|
||||||
|
className="flex items-center gap-2"
|
||||||
>
|
>
|
||||||
<span className="dark:text-slate-200">{getLabel(val)}</span>
|
<span className="dark:text-slate-200">{getLabel(val)}</span>
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MessageState, MessageType } from "@core/stores/messageStore/index.ts";
|
import { MessageState, MessageType } from "@core/stores";
|
||||||
import type { Message } from "@core/stores/messageStore/types.ts";
|
import type { Message } from "@core/stores/messageStore/types.ts";
|
||||||
import type { Types } from "@meshtastic/core";
|
import type { Types } from "@meshtastic/core";
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user