270 Commits
Author SHA1 Message Date
hermes 9481b6614a fix(connections): allow toggling HTTPS off in Add Connection dialog
Push to Main CI / build-and-package (push) Canceled after 0s
The 'Use HTTPS' switch was disabled whenever the web client itself was
served over HTTPS, making it impossible to add a plain-HTTP node.
Keep the switch always interactive and instead show a mixed-content
warning when HTTP is selected on an HTTPS-served page.

Adds unit tests for the protocol toggle.
2026-08-30 20:46:39 +00:00
Dan DitomasoandGitHub cffd35fe05 Merge pull request #1400 from meshtastic/dependabot/npm_and_yarn/oxfmt-0.63.0
Push to Main CI / build-and-package (push) Canceled after 0s
chore(deps-dev): bump oxfmt from 0.61.0 to 0.63.0
2026-08-23 01:48:53 +00:00
Dan DitomasoandGitHub a2036802cf Merge pull request #1398 from meshtastic/dependabot/npm_and_yarn/testing-library/user-event-14.6.4
chore(deps-dev): bump @testing-library/user-event from 14.6.1 to 14.6.4
2026-08-23 01:48:50 +00:00
Dan DitomasoandGitHub 3d88827331 Merge pull request #1399 from meshtastic/dependabot/npm_and_yarn/maplibre-gl-6.3.0
chore(deps): bump maplibre-gl from 6.0.0 to 6.3.0
2026-08-23 01:48:49 +00:00
Dan DitomasoandGitHub 2c4abbf128 Merge branch 'main' into dependabot/npm_and_yarn/testing-library/user-event-14.6.4 2026-08-22 21:46:39 -04:00
Dan DitomasoandGitHub 8cb2f16b19 Merge branch 'main' into dependabot/npm_and_yarn/maplibre-gl-6.3.0 2026-08-22 21:46:13 -04:00
Dan DitomasoandGitHub 1c254b707f Merge branch 'main' into dependabot/npm_and_yarn/oxfmt-0.63.0 2026-08-22 21:46:02 -04:00
Dan DitomasoandGitHub 77bf54be2a Merge pull request #1370 from meshtastic/dependabot/github_actions/docker/login-action-4.5.2
chore(deps): bump docker/login-action from 4 to 4.5.2
2026-08-23 01:33:48 +00:00
Dan DitomasoandGitHub 58d09a1d47 Merge branch 'main' into dependabot/github_actions/docker/login-action-4.5.2 2026-08-22 21:33:39 -04:00
dependabot[bot]andGitHub 1fdfd76f42 chore(deps-dev): bump oxfmt from 0.61.0 to 0.63.0
Bumps [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) from 0.61.0 to 0.63.0.
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.63.0/npm/oxfmt)

---
updated-dependencies:
- dependency-name: oxfmt
  dependency-version: 0.63.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-16 08:51:45 +00:00
dependabot[bot]andGitHub 722766b0a4 chore(deps): bump maplibre-gl from 6.0.0 to 6.3.0
Bumps [maplibre-gl](https://github.com/maplibre/maplibre-gl-js) from 6.0.0 to 6.3.0.
- [Release notes](https://github.com/maplibre/maplibre-gl-js/releases)
- [Changelog](https://github.com/maplibre/maplibre-gl-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/maplibre/maplibre-gl-js/compare/v6.0.0...v6.3.0)

---
updated-dependencies:
- dependency-name: maplibre-gl
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-16 08:51:16 +00:00
dependabot[bot]andGitHub 369b852080 chore(deps-dev): bump @testing-library/user-event from 14.6.1 to 14.6.4
Bumps [@testing-library/user-event](https://github.com/testing-library/user-event) from 14.6.1 to 14.6.4.
- [Release notes](https://github.com/testing-library/user-event/releases)
- [Changelog](https://github.com/testing-library/user-event/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/user-event/compare/v14.6.1...v14.6.4)

---
updated-dependencies:
- dependency-name: "@testing-library/user-event"
  dependency-version: 14.6.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-16 08:49:13 +00:00
Dan DitomasoandGitHub 1db40fe80b Merge pull request #1375 from nikolya-prodigy/fix/react-map-gl-maplibre-6
fix(deps): update react-map-gl for MapLibre GL 6
2026-08-10 12:33:16 +00:00
Dan DitomasoandGitHub 6c691c7ecb Merge branch 'main' into fix/react-map-gl-maplibre-6 2026-08-10 08:32:33 -04:00
Hunter ThornsberryandGitHub 0142a28d89 Merge pull request #1383 from meshtastic/dependabot/npm_and_yarn/fast-uri-3.1.5
chore(deps): bump fast-uri from 3.1.4 to 3.1.5
2026-08-09 02:40:20 +00:00
d9e928d682 fix: update pnpm-lock.yaml with only fast-uri 3.1.4→3.1.5 bump
Dependabot's lockfile update also pulled in 8 packages published
within the minimumReleaseAge window (2 days), causing pnpm's
supply-chain policy check to fail in CI. Restore lockfile to the
parent state and apply only the minimal 4-line fast-uri change.

Co-authored-by: Hunter275 <3261295+Hunter275@users.noreply.github.com>
2026-08-08 19:52:04 +00:00
dependabot[bot]andGitHub d452d99ef0 chore(deps): bump fast-uri from 3.1.4 to 3.1.5
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.4 to 3.1.5.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.4...v3.1.5)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.5
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-08 19:40:13 +00:00
Hunter ThornsberryandGitHub ee5243a205 Merge pull request #1377 from meshtastic/dependabot/npm_and_yarn/i18next-http-backend-4.0.1
chore(deps): bump i18next-http-backend from 4.0.0 to 4.0.1
2026-08-08 19:37:23 +00:00
Hunter ThornsberryandGitHub 37c0fedeec Merge pull request #1378 from meshtastic/dependabot/npm_and_yarn/jsdom-30.0.1
chore(deps-dev): bump jsdom from 30.0.0 to 30.0.1
2026-08-08 19:37:09 +00:00
Hunter ThornsberryandGitHub d10b71c6f5 Merge pull request #1379 from meshtastic/dependabot/npm_and_yarn/react-map-gl-8.1.2
chore(deps): bump react-map-gl from 8.1.1 to 8.1.2
2026-08-08 19:37:00 +00:00
Hunter ThornsberryandGitHub 7de9b1c943 Merge pull request #1380 from meshtastic/dependabot/npm_and_yarn/lucide-react-1.28.0
chore(deps): bump lucide-react from 1.27.0 to 1.28.0
2026-08-08 19:36:49 +00:00
dependabot[bot]andGitHub 3f5086eb82 chore(deps): bump lucide-react from 1.27.0 to 1.28.0
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 1.27.0 to 1.28.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.28.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.28.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-02 08:36:13 +00:00
dependabot[bot]andGitHub 6d3acd85ca chore(deps): bump react-map-gl from 8.1.1 to 8.1.2
Bumps [react-map-gl](https://github.com/visgl/react-map-gl) from 8.1.1 to 8.1.2.
- [Release notes](https://github.com/visgl/react-map-gl/releases)
- [Changelog](https://github.com/visgl/react-map-gl/blob/v8.1.2/CHANGELOG.md)
- [Commits](https://github.com/visgl/react-map-gl/compare/v8.1.1...v8.1.2)

---
updated-dependencies:
- dependency-name: react-map-gl
  dependency-version: 8.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-02 08:35:32 +00:00
dependabot[bot]andGitHub 944de7d560 chore(deps-dev): bump jsdom from 30.0.0 to 30.0.1
Bumps [jsdom](https://github.com/jsdom/jsdom) from 30.0.0 to 30.0.1.
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Commits](https://github.com/jsdom/jsdom/compare/v30.0.0...v30.0.1)

---
updated-dependencies:
- dependency-name: jsdom
  dependency-version: 30.0.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-02 08:34:51 +00:00
dependabot[bot]andGitHub a1088b733a chore(deps): bump i18next-http-backend from 4.0.0 to 4.0.1
Bumps [i18next-http-backend](https://github.com/i18next/i18next-http-backend) from 4.0.0 to 4.0.1.
- [Changelog](https://github.com/i18next/i18next-http-backend/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next-http-backend/compare/v4.0.0...v4.0.1)

---
updated-dependencies:
- dependency-name: i18next-http-backend
  dependency-version: 4.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-02 08:34:16 +00:00
NIKOLYA PRODIGY d2cf940df1 fix(deps): support MapLibre GL 6 2026-08-01 15:03:04 +03:00
Hunter ThornsberryandGitHub d920c7e044 Merge branch 'main' into dependabot/github_actions/docker/login-action-4.5.2 2026-07-31 23:15:47 -04:00
Hunter ThornsberryandGitHub 9a474420f2 Merge pull request #1372 from meshtastic/dependabot/npm_and_yarn/types/node-26.1.2
chore(deps): bump @types/node from 26.1.1 to 26.1.2
2026-08-01 03:15:40 +00:00
dependabot[bot]andGitHub 65aefbfb51 chore(deps): bump @types/node from 26.1.1 to 26.1.2
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 26.1.1 to 26.1.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-01 01:58:47 +00:00
Hunter ThornsberryandGitHub cc117b93aa Merge pull request #1363 from meshtastic/dependabot/npm_and_yarn/publint-0.3.22
chore(deps-dev): bump publint from 0.3.21 to 0.3.22
2026-08-01 01:45:00 +00:00
dependabot[bot]andGitHub e3b84abaae chore(deps-dev): bump publint from 0.3.21 to 0.3.22
Bumps [publint](https://github.com/publint/publint/tree/HEAD/packages/publint) from 0.3.21 to 0.3.22.
- [Release notes](https://github.com/publint/publint/releases)
- [Changelog](https://github.com/publint/publint/blob/master/packages/publint/CHANGELOG.md)
- [Commits](https://github.com/publint/publint/commits/HEAD/packages/publint)

---
updated-dependencies:
- dependency-name: publint
  dependency-version: 0.3.22
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-01 01:44:55 +00:00
Hunter ThornsberryandGitHub 38119f2e2b Merge pull request #1365 from meshtastic/dependabot/npm_and_yarn/lucide-react-1.27.0
chore(deps): bump lucide-react from 1.26.0 to 1.27.0
2026-08-01 01:40:54 +00:00
Hunter ThornsberryandGitHub 5a98701f51 Merge pull request #1367 from meshtastic/dependabot/npm_and_yarn/hookform/resolvers-5.5.7
chore(deps): bump @hookform/resolvers from 5.4.2 to 5.5.7
2026-08-01 01:40:42 +00:00
Hunter ThornsberryandGitHub c50036ceea Merge pull request #1373 from meshtastic/dependabot/npm_and_yarn/oxfmt-0.61.0
chore(deps-dev): bump oxfmt from 0.60.0 to 0.61.0
2026-08-01 01:39:28 +00:00
Hunter ThornsberryandGitHub 7b0d334626 Merge pull request #1362 from meshtastic/dependabot/npm_and_yarn/vitejs/plugin-react-6.0.4
chore(deps-dev): bump @vitejs/plugin-react from 6.0.3 to 6.0.4
2026-08-01 01:39:13 +00:00
Hunter ThornsberryandGitHub 0374317968 Merge branch 'main' into dependabot/github_actions/docker/login-action-4.5.2 2026-07-31 21:12:11 -04:00
dependabot[bot]andGitHub bddf99aef2 chore(deps-dev): bump oxfmt from 0.60.0 to 0.61.0
Bumps [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) from 0.60.0 to 0.61.0.
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.61.0/npm/oxfmt)

---
updated-dependencies:
- dependency-name: oxfmt
  dependency-version: 0.61.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-31 06:14:02 +00:00
dependabot[bot]andGitHub c6bb3cb65f chore(deps): bump @hookform/resolvers from 5.4.2 to 5.5.7
Bumps [@hookform/resolvers](https://github.com/react-hook-form/resolvers) from 5.4.2 to 5.5.7.
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v5.4.2...v5.5.7)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
  dependency-version: 5.5.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-31 06:13:15 +00:00
dependabot[bot]andGitHub 87a4226c21 chore(deps): bump lucide-react from 1.26.0 to 1.27.0
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 1.26.0 to 1.27.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.27.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-31 06:13:07 +00:00
dependabot[bot]andGitHub 6ba9488f19 chore(deps-dev): bump @vitejs/plugin-react from 6.0.3 to 6.0.4
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 6.0.3 to 6.0.4.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.4/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-31 06:12:14 +00:00
Hunter ThornsberryandGitHub a574db005c Merge pull request #1368 from meshtastic/dependabot/npm_and_yarn/jsdom-30.0.0
chore(deps-dev): bump jsdom from 29.1.1 to 30.0.0
2026-07-31 05:43:16 +00:00
Hunter ThornsberryandGitHub ea5c033c04 Merge branch 'main' into dependabot/npm_and_yarn/jsdom-30.0.0 2026-07-30 20:15:08 -04:00
dependabot[bot]andGitHub 68adbe4cac chore(deps): bump docker/login-action from 4 to 4.5.2
Bumps [docker/login-action](https://github.com/docker/login-action) from 4 to 4.5.2.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v4...v4.5.2)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: 4.5.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-31 00:10:52 +00:00
Hunter ThornsberryandGitHub b968bf9908 Merge pull request #1366 from Hunter275/dependabot-weekly
Set dependabot to be weekly instead of daily
2026-07-30 23:55:38 +00:00
copilot-swe-agent[bot]andGitHub 185933e779 fix: remove invalid tsconfig project references that break Playwright e2e
The root tsconfig.json referenced apps/web and packages/transport-http as
TypeScript project references, but those tsconfigs lacked the required
`composite: true` setting. Playwright 1.62.0 strictly validates tsconfig
project references when loading playwright.config.ts, causing the e2e CI
job to fail with:

  Error: Failed to load tsconfig file at .../tsconfig.json:
  Failed to resolve "references" path "apps/web" referenced from .../tsconfig.json

Remove the invalid references. Individual package tsconfigs already extend
tsconfig.base.json directly and remain unaffected.
2026-07-30 14:52:42 +00:00
dependabot[bot]andGitHub 2e352810c3 chore(deps-dev): bump jsdom from 29.1.1 to 30.0.0
Bumps [jsdom](https://github.com/jsdom/jsdom) from 29.1.1 to 30.0.0.
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Commits](https://github.com/jsdom/jsdom/compare/v29.1.1...v30.0.0)

---
updated-dependencies:
- dependency-name: jsdom
  dependency-version: 30.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-30 08:35:16 +00:00
Hunter275 fce9f034f5 fix for missed daily 2026-07-29 21:19:51 -04:00
Hunter275 afd8db01d2 set dependabot to be weekly instead of daily 2026-07-29 21:14:19 -04:00
Hunter ThornsberryandGitHub 6894b5fe9c Merge pull request #1351 from meshtastic/dependabot/npm_and_yarn/radix-ui-cecebef696
chore(deps): bump the radix-ui group with 20 updates
2026-07-29 01:56:08 +00:00
Hunter ThornsberryandGitHub e89814818c Merge branch 'main' into dependabot/npm_and_yarn/radix-ui-cecebef696 2026-07-28 21:55:08 -04:00
Hunter ThornsberryandGitHub 904c7107f3 Merge pull request #1350 from meshtastic/dependabot/github_actions/actions/stale-11
chore(deps): bump actions/stale from 10 to 11
2026-07-29 01:55:06 +00:00
Hunter ThornsberryandGitHub 4f0b07c403 Merge branch 'main' into dependabot/github_actions/actions/stale-11 2026-07-28 21:54:37 -04:00
Hunter ThornsberryandGitHub 542172f119 Merge branch 'main' into dependabot/github_actions/actions/stale-11 2026-07-28 21:54:26 -04:00
Hunter ThornsberryandGitHub b1344af667 Merge branch 'main' into dependabot/npm_and_yarn/radix-ui-cecebef696 2026-07-28 21:54:20 -04:00
Hunter ThornsberryandGitHub 695d6cd9a8 Merge pull request #1352 from meshtastic/dependabot/npm_and_yarn/hookform/resolvers-5.4.2
chore(deps): bump @hookform/resolvers from 5.4.0 to 5.4.2
2026-07-29 01:54:13 +00:00
Hunter ThornsberryandGitHub 5f4f1094ff Merge pull request #1353 from meshtastic/dependabot/npm_and_yarn/maplibre-gl-6.0.0
chore(deps): bump maplibre-gl from 5.24.0 to 6.0.0
2026-07-29 01:54:11 +00:00
Hunter ThornsberryandGitHub 2b752eb807 Merge pull request #1354 from meshtastic/dependabot/npm_and_yarn/tar-7.5.22
chore(deps-dev): bump tar from 7.5.21 to 7.5.22
2026-07-29 01:54:08 +00:00
Hunter ThornsberryandGitHub 17ebb0f5bd Merge pull request #1356 from meshtastic/dependabot/npm_and_yarn/happy-dom-20.11.1
chore(deps-dev): bump happy-dom from 20.11.0 to 20.11.1
2026-07-29 01:54:02 +00:00
Hunter ThornsberryandGitHub 9ef76f6d6b Merge pull request #1357 from meshtastic/dependabot/npm_and_yarn/react-i18next-17.0.11
chore(deps): bump react-i18next from 17.0.10 to 17.0.11
2026-07-29 01:54:00 +00:00
Hunter ThornsberryandGitHub e1ef436de5 Merge pull request #1358 from meshtastic/dependabot/npm_and_yarn/playwright/test-1.62.0
chore(deps-dev): bump @playwright/test from 1.61.1 to 1.62.0
2026-07-29 01:53:57 +00:00
Hunter ThornsberryandGitHub f32ef36223 Merge pull request #1359 from meshtastic/dependabot/npm_and_yarn/oxlint-1.75.0
chore(deps-dev): bump oxlint from 1.74.0 to 1.75.0
2026-07-29 01:53:54 +00:00
Hunter ThornsberryandGitHub 7cd8b8eb9a Merge pull request #1360 from meshtastic/dependabot/npm_and_yarn/testing-library/jest-dom-7.0.0
chore(deps-dev): bump @testing-library/jest-dom from 6.9.1 to 7.0.0
2026-07-29 01:53:52 +00:00
dependabot[bot]andGitHub 4b3ccf92f9 chore(deps-dev): bump @testing-library/jest-dom from 6.9.1 to 7.0.0
Bumps [@testing-library/jest-dom](https://github.com/testing-library/jest-dom) from 6.9.1 to 7.0.0.
- [Release notes](https://github.com/testing-library/jest-dom/releases)
- [Changelog](https://github.com/testing-library/jest-dom/blob/main/CHANGELOG.md)
- [Commits](https://github.com/testing-library/jest-dom/compare/v6.9.1...v7.0.0)

---
updated-dependencies:
- dependency-name: "@testing-library/jest-dom"
  dependency-version: 7.0.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28 08:47:44 +00:00
dependabot[bot]andGitHub 9d6f89105a chore(deps-dev): bump oxlint from 1.74.0 to 1.75.0
Bumps [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) from 1.74.0 to 1.75.0.
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.75.0/npm/oxlint)

---
updated-dependencies:
- dependency-name: oxlint
  dependency-version: 1.75.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28 08:47:12 +00:00
dependabot[bot]andGitHub 851f6324d8 chore(deps-dev): bump @playwright/test from 1.61.1 to 1.62.0
Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.61.1 to 1.62.0.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.61.1...v1.62.0)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.62.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28 08:46:39 +00:00
dependabot[bot]andGitHub 88f61b7588 chore(deps): bump react-i18next from 17.0.10 to 17.0.11
Bumps [react-i18next](https://github.com/i18next/react-i18next) from 17.0.10 to 17.0.11.
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v17.0.10...v17.0.11)

---
updated-dependencies:
- dependency-name: react-i18next
  dependency-version: 17.0.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28 08:46:06 +00:00
dependabot[bot]andGitHub 696bb0b839 chore(deps-dev): bump happy-dom from 20.11.0 to 20.11.1
Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 20.11.0 to 20.11.1.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.11.0...v20.11.1)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-version: 20.11.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28 08:45:27 +00:00
dependabot[bot]andGitHub 82e6103fc1 chore(deps-dev): bump tar from 7.5.21 to 7.5.22
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.21 to 7.5.22.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.21...v7.5.22)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.22
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28 08:44:24 +00:00
dependabot[bot]andGitHub b30974355a chore(deps): bump maplibre-gl from 5.24.0 to 6.0.0
Bumps [maplibre-gl](https://github.com/maplibre/maplibre-gl-js) from 5.24.0 to 6.0.0.
- [Release notes](https://github.com/maplibre/maplibre-gl-js/releases)
- [Changelog](https://github.com/maplibre/maplibre-gl-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/maplibre/maplibre-gl-js/compare/v5.24.0...v6.0.0)

---
updated-dependencies:
- dependency-name: maplibre-gl
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28 08:43:49 +00:00
dependabot[bot]andGitHub 523784f9c7 chore(deps): bump @hookform/resolvers from 5.4.0 to 5.4.2
Bumps [@hookform/resolvers](https://github.com/react-hook-form/resolvers) from 5.4.0 to 5.4.2.
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v5.4.0...v5.4.2)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
  dependency-version: 5.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28 08:43:17 +00:00
dependabot[bot]andGitHub f15f7f5be0 chore(deps): bump the radix-ui group with 20 updates
Bumps the radix-ui group with 20 updates:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-collapsible](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/collapsible) | `1.1.18` | `1.1.20` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.21` | `1.1.23` |
| [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.22` | `2.1.24` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.13` | `1.1.15` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.3.1` | `1.3.3` |
| [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.2.14` | `1.2.16` |
| [@radix-ui/react-accordion](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/accordion) | `1.2.18` | `1.2.20` |
| [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/alert-dialog) | `1.1.21` | `1.1.23` |
| [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) | `1.3.9` | `1.3.11` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.13` | `2.1.15` |
| [@radix-ui/react-menubar](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/menubar) | `1.1.22` | `1.1.24` |
| [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) | `1.1.21` | `1.1.23` |
| [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) | `1.2.16` | `1.2.18` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.3.5` | `2.3.7` |
| [@radix-ui/react-slider](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slider) | `1.4.5` | `1.4.7` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.3.5` | `1.3.7` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.19` | `1.1.21` |
| [@radix-ui/react-toast](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toast) | `1.2.21` | `1.2.23` |
| [@radix-ui/react-toggle-group](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toggle-group) | `1.1.17` | `1.1.19` |
| [@radix-ui/react-visually-hidden](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/visually-hidden) | `1.2.9` | `1.2.11` |


Updates `@radix-ui/react-collapsible` from 1.1.18 to 1.1.20
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/collapsible/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/collapsible)

Updates `@radix-ui/react-dialog` from 1.1.21 to 1.1.23
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-dropdown-menu` from 2.1.22 to 2.1.24
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu)

Updates `@radix-ui/react-separator` from 1.1.13 to 1.1.15
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator)

Updates `@radix-ui/react-slot` from 1.3.1 to 1.3.3
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot)

Updates `@radix-ui/react-tooltip` from 1.2.14 to 1.2.16
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip)

Updates `@radix-ui/react-accordion` from 1.2.18 to 1.2.20
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/accordion/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/accordion)

Updates `@radix-ui/react-alert-dialog` from 1.1.21 to 1.1.23
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/alert-dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/alert-dialog)

Updates `@radix-ui/react-checkbox` from 1.3.9 to 1.3.11
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/checkbox)

Updates `@radix-ui/react-label` from 2.1.13 to 2.1.15
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

Updates `@radix-ui/react-menubar` from 1.1.22 to 1.1.24
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/menubar/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/menubar)

Updates `@radix-ui/react-popover` from 1.1.21 to 1.1.23
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover)

Updates `@radix-ui/react-scroll-area` from 1.2.16 to 1.2.18
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/scroll-area)

Updates `@radix-ui/react-select` from 2.3.5 to 2.3.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select)

Updates `@radix-ui/react-slider` from 1.4.5 to 1.4.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slider/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slider)

Updates `@radix-ui/react-switch` from 1.3.5 to 1.3.7
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

Updates `@radix-ui/react-tabs` from 1.1.19 to 1.1.21
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs)

Updates `@radix-ui/react-toast` from 1.2.21 to 1.2.23
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toast/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toast)

Updates `@radix-ui/react-toggle-group` from 1.1.17 to 1.1.19
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toggle-group/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toggle-group)

Updates `@radix-ui/react-visually-hidden` from 1.2.9 to 1.2.11
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/visually-hidden/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/visually-hidden)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-collapsible"
  dependency-version: 1.1.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-accordion"
  dependency-version: 1.2.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-alert-dialog"
  dependency-version: 1.1.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-checkbox"
  dependency-version: 1.3.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-menubar"
  dependency-version: 1.1.24
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-popover"
  dependency-version: 1.1.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-scroll-area"
  dependency-version: 1.2.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-slider"
  dependency-version: 1.4.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-toast"
  dependency-version: 1.2.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-toggle-group"
  dependency-version: 1.1.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-visually-hidden"
  dependency-version: 1.2.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28 08:42:40 +00:00
dependabot[bot]andGitHub 02e02a72df chore(deps): bump actions/stale from 10 to 11
Bumps [actions/stale](https://github.com/actions/stale) from 10 to 11.
- [Release notes](https://github.com/actions/stale/releases)
- [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/stale/compare/v10...v11)

---
updated-dependencies:
- dependency-name: actions/stale
  dependency-version: '11'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-28 08:34:03 +00:00
Dan DitomasoandGitHub 5b10d3919d Merge pull request #1305 from meshtastic/dependabot/npm_and_yarn/tanstack-4e0cd3caa5
chore(deps): bump the tanstack group across 1 directory with 3 updates
2026-07-28 01:13:56 +00:00
Dan Ditomaso 1066d1ff6a Merge main into tanstack bump 2026-07-27 21:10:27 -04:00
Hunter ThornsberryandGitHub 8e611a6a03 Merge pull request #1342 from meshtastic/dependabot/npm_and_yarn/react-ecosystem-479fbeca4e
chore(deps): bump the react-ecosystem group across 1 directory with 2 updates
2026-07-27 19:09:20 +00:00
dependabot[bot]andGitHub 12a998242a chore(deps): bump the react-ecosystem group across 1 directory with 2 updates
Bumps the react-ecosystem group with 2 updates in the / directory: [react](https://github.com/react/react/tree/HEAD/packages/react) and [react-dom](https://github.com/react/react/tree/HEAD/packages/react-dom).


Updates `react` from 19.2.7 to 19.2.8
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.2.8/packages/react)

Updates `react-dom` from 19.2.7 to 19.2.8
- [Release notes](https://github.com/react/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/react/react/commits/v19.2.8/packages/react-dom)

---
updated-dependencies:
- dependency-name: react
  dependency-version: 19.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: react-ecosystem
- dependency-name: react-dom
  dependency-version: 19.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: react-ecosystem
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 18:29:30 +00:00
Hunter ThornsberryandGitHub 0c2bdd33c7 Merge pull request #1343 from meshtastic/dependabot/npm_and_yarn/oxfmt-0.60.0
chore(deps-dev): bump oxfmt from 0.59.0 to 0.60.0
2026-07-27 18:27:12 +00:00
Hunter ThornsberryandGitHub daa71c5516 Merge branch 'main' into dependabot/npm_and_yarn/oxfmt-0.60.0 2026-07-27 14:27:02 -04:00
Hunter ThornsberryandGitHub c3e2df6672 Merge pull request #1341 from meshtastic/dependabot/npm_and_yarn/radix-ui-48a6414ad5
chore(deps): bump the radix-ui group with 20 updates
2026-07-27 18:26:12 +00:00
Hunter ThornsberryandGitHub 86cb621f10 Merge pull request #1345 from meshtastic/dependabot/npm_and_yarn/bufbuild/protoc-gen-es-2.13.0
chore(deps-dev): bump @bufbuild/protoc-gen-es from 2.12.1 to 2.13.0
2026-07-27 18:26:05 +00:00
Hunter ThornsberryandGitHub 192944f48c Merge branch 'main' into dependabot/npm_and_yarn/bufbuild/protoc-gen-es-2.13.0 2026-07-27 14:17:07 -04:00
Hunter ThornsberryandGitHub 479d9d333c Merge branch 'main' into dependabot/npm_and_yarn/radix-ui-48a6414ad5 2026-07-27 14:15:54 -04:00
Hunter ThornsberryandGitHub b5a96dcc2e Merge pull request #1344 from meshtastic/dependabot/npm_and_yarn/lint-staged-17.2.0
chore(deps-dev): bump lint-staged from 17.1.0 to 17.2.0
2026-07-27 18:15:37 +00:00
Hunter ThornsberryandGitHub 1699097683 Merge pull request #1346 from meshtastic/dependabot/npm_and_yarn/lucide-react-1.26.0
chore(deps): bump lucide-react from 1.25.0 to 1.26.0
2026-07-27 18:15:31 +00:00
Hunter ThornsberryandGitHub 1577f59272 Merge pull request #1347 from meshtastic/dependabot/npm_and_yarn/crypto-random-string-6.0.0
chore(deps): bump crypto-random-string from 5.0.0 to 6.0.0
2026-07-27 18:15:29 +00:00
Hunter ThornsberryandGitHub 1954715077 Merge pull request #1348 from meshtastic/dependabot/npm_and_yarn/bufbuild/protobuf-2.13.0
chore(deps): bump @bufbuild/protobuf from 2.12.1 to 2.13.0
2026-07-27 18:15:26 +00:00
Hunter ThornsberryandGitHub c81a481e00 Merge pull request #1349 from meshtastic/dependabot/npm_and_yarn/tsdown-0.22.14
chore(deps-dev): bump tsdown from 0.22.13 to 0.22.14
2026-07-27 18:15:22 +00:00
dependabot[bot]andGitHub 50d4923ad3 chore(deps-dev): bump tsdown from 0.22.13 to 0.22.14
Bumps [tsdown](https://github.com/rolldown/tsdown) from 0.22.13 to 0.22.14.
- [Release notes](https://github.com/rolldown/tsdown/releases)
- [Commits](https://github.com/rolldown/tsdown/compare/v0.22.13...v0.22.14)

---
updated-dependencies:
- dependency-name: tsdown
  dependency-version: 0.22.14
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 08:44:11 +00:00
dependabot[bot]andGitHub 0c272cdc1e chore(deps): bump @bufbuild/protobuf from 2.12.1 to 2.13.0
Bumps [@bufbuild/protobuf](https://github.com/bufbuild/protobuf-es/tree/HEAD/packages/protobuf) from 2.12.1 to 2.13.0.
- [Release notes](https://github.com/bufbuild/protobuf-es/releases)
- [Commits](https://github.com/bufbuild/protobuf-es/commits/v2.13.0/packages/protobuf)

---
updated-dependencies:
- dependency-name: "@bufbuild/protobuf"
  dependency-version: 2.13.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 08:43:47 +00:00
dependabot[bot]andGitHub 9996c59535 chore(deps): bump crypto-random-string from 5.0.0 to 6.0.0
Bumps [crypto-random-string](https://github.com/sindresorhus/crypto-random-string) from 5.0.0 to 6.0.0.
- [Release notes](https://github.com/sindresorhus/crypto-random-string/releases)
- [Commits](https://github.com/sindresorhus/crypto-random-string/compare/v5.0.0...v6.0.0)

---
updated-dependencies:
- dependency-name: crypto-random-string
  dependency-version: 6.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 08:43:26 +00:00
dependabot[bot]andGitHub 2e121c4c3b chore(deps): bump lucide-react from 1.25.0 to 1.26.0
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 1.25.0 to 1.26.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.26.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 08:43:04 +00:00
dependabot[bot]andGitHub 726075b0ab chore(deps-dev): bump @bufbuild/protoc-gen-es from 2.12.1 to 2.13.0
Bumps [@bufbuild/protoc-gen-es](https://github.com/bufbuild/protobuf-es/tree/HEAD/packages/protoc-gen-es) from 2.12.1 to 2.13.0.
- [Release notes](https://github.com/bufbuild/protobuf-es/releases)
- [Commits](https://github.com/bufbuild/protobuf-es/commits/v2.13.0/packages/protoc-gen-es)

---
updated-dependencies:
- dependency-name: "@bufbuild/protoc-gen-es"
  dependency-version: 2.13.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 08:42:38 +00:00
dependabot[bot]andGitHub 46c2b5f262 chore(deps-dev): bump lint-staged from 17.1.0 to 17.2.0
Bumps [lint-staged](https://github.com/lint-staged/lint-staged) from 17.1.0 to 17.2.0.
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v17.1.0...v17.2.0)

---
updated-dependencies:
- dependency-name: lint-staged
  dependency-version: 17.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 08:42:04 +00:00
dependabot[bot]andGitHub 5d97a27bc3 chore(deps-dev): bump oxfmt from 0.59.0 to 0.60.0
Bumps [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) from 0.59.0 to 0.60.0.
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.60.0/npm/oxfmt)

---
updated-dependencies:
- dependency-name: oxfmt
  dependency-version: 0.60.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 08:41:39 +00:00
dependabot[bot]andGitHub bcee361006 chore(deps): bump the radix-ui group with 20 updates
Bumps the radix-ui group with 20 updates:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-collapsible](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/collapsible) | `1.1.17` | `1.1.18` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.20` | `1.1.21` |
| [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.21` | `2.1.22` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.12` | `1.1.13` |
| [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) | `1.3.0` | `1.3.1` |
| [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.2.13` | `1.2.14` |
| [@radix-ui/react-accordion](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/accordion) | `1.2.17` | `1.2.18` |
| [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/alert-dialog) | `1.1.20` | `1.1.21` |
| [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) | `1.3.8` | `1.3.9` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.12` | `2.1.13` |
| [@radix-ui/react-menubar](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/menubar) | `1.1.21` | `1.1.22` |
| [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) | `1.1.20` | `1.1.21` |
| [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) | `1.2.15` | `1.2.16` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.3.4` | `2.3.5` |
| [@radix-ui/react-slider](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slider) | `1.4.4` | `1.4.5` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.3.4` | `1.3.5` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.18` | `1.1.19` |
| [@radix-ui/react-toast](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toast) | `1.2.20` | `1.2.21` |
| [@radix-ui/react-toggle-group](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toggle-group) | `1.1.16` | `1.1.17` |
| [@radix-ui/react-visually-hidden](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/visually-hidden) | `1.2.8` | `1.2.9` |


Updates `@radix-ui/react-collapsible` from 1.1.17 to 1.1.18
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/collapsible/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/collapsible)

Updates `@radix-ui/react-dialog` from 1.1.20 to 1.1.21
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-dropdown-menu` from 2.1.21 to 2.1.22
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu)

Updates `@radix-ui/react-separator` from 1.1.12 to 1.1.13
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator)

Updates `@radix-ui/react-slot` from 1.3.0 to 1.3.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot)

Updates `@radix-ui/react-tooltip` from 1.2.13 to 1.2.14
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip)

Updates `@radix-ui/react-accordion` from 1.2.17 to 1.2.18
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/accordion/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/accordion)

Updates `@radix-ui/react-alert-dialog` from 1.1.20 to 1.1.21
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/alert-dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/alert-dialog)

Updates `@radix-ui/react-checkbox` from 1.3.8 to 1.3.9
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/checkbox)

Updates `@radix-ui/react-label` from 2.1.12 to 2.1.13
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

Updates `@radix-ui/react-menubar` from 1.1.21 to 1.1.22
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/menubar/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/menubar)

Updates `@radix-ui/react-popover` from 1.1.20 to 1.1.21
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover)

Updates `@radix-ui/react-scroll-area` from 1.2.15 to 1.2.16
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/scroll-area)

Updates `@radix-ui/react-select` from 2.3.4 to 2.3.5
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select)

Updates `@radix-ui/react-slider` from 1.4.4 to 1.4.5
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slider/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slider)

Updates `@radix-ui/react-switch` from 1.3.4 to 1.3.5
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

Updates `@radix-ui/react-tabs` from 1.1.18 to 1.1.19
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs)

Updates `@radix-ui/react-toast` from 1.2.20 to 1.2.21
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toast/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toast)

Updates `@radix-ui/react-toggle-group` from 1.1.16 to 1.1.17
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toggle-group/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toggle-group)

Updates `@radix-ui/react-visually-hidden` from 1.2.8 to 1.2.9
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/visually-hidden/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/visually-hidden)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-collapsible"
  dependency-version: 1.1.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-accordion"
  dependency-version: 1.2.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-alert-dialog"
  dependency-version: 1.1.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-checkbox"
  dependency-version: 1.3.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-menubar"
  dependency-version: 1.1.22
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-popover"
  dependency-version: 1.1.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-scroll-area"
  dependency-version: 1.2.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-slider"
  dependency-version: 1.4.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-toast"
  dependency-version: 1.2.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-toggle-group"
  dependency-version: 1.1.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-visually-hidden"
  dependency-version: 1.2.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-27 08:40:21 +00:00
copilot-swe-agent[bot]andGitHub 9ee62f0866 Merge remote-tracking branch 'origin/main' into dependabot/npm_and_yarn/tanstack-4e0cd3caa5
# Conflicts:
#	apps/web/package.json
#	packages/ui/package.json
#	pnpm-lock.yaml
2026-07-25 21:32:31 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Dan Ditomaso
8ffa89a503 chore(deps-dev): bump tar from 7.5.20 to 7.5.21 (#1339)
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.20 to 7.5.21.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.20...v7.5.21)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.21
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dan Ditomaso <dan.ditomaso@gmail.com>
2026-07-25 14:45:08 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
35fe0a261b chore(deps): bump the radix-ui group across 1 directory with 19 updates (#1327)
Bumps the radix-ui group with 19 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [@radix-ui/react-collapsible](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/collapsible) | `1.1.16` | `1.1.17` |
| [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) | `1.1.19` | `1.1.20` |
| [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) | `2.1.20` | `2.1.21` |
| [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) | `1.1.11` | `1.1.12` |
| [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) | `1.2.12` | `1.2.13` |
| [@radix-ui/react-accordion](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/accordion) | `1.2.16` | `1.2.17` |
| [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/alert-dialog) | `1.1.19` | `1.1.20` |
| [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) | `1.3.7` | `1.3.8` |
| [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) | `2.1.11` | `2.1.12` |
| [@radix-ui/react-menubar](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/menubar) | `1.1.20` | `1.1.21` |
| [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) | `1.1.19` | `1.1.20` |
| [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) | `1.2.14` | `1.2.15` |
| [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) | `2.3.3` | `2.3.4` |
| [@radix-ui/react-slider](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slider) | `1.4.3` | `1.4.4` |
| [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) | `1.3.3` | `1.3.4` |
| [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) | `1.1.17` | `1.1.18` |
| [@radix-ui/react-toast](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toast) | `1.2.19` | `1.2.20` |
| [@radix-ui/react-toggle-group](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toggle-group) | `1.1.15` | `1.1.16` |
| [@radix-ui/react-visually-hidden](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/visually-hidden) | `1.2.7` | `1.2.8` |



Updates `@radix-ui/react-collapsible` from 1.1.16 to 1.1.17
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/collapsible/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/collapsible)

Updates `@radix-ui/react-dialog` from 1.1.19 to 1.1.20
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

Updates `@radix-ui/react-dropdown-menu` from 2.1.20 to 2.1.21
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu)

Updates `@radix-ui/react-separator` from 1.1.11 to 1.1.12
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator)

Updates `@radix-ui/react-tooltip` from 1.2.12 to 1.2.13
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip)

Updates `@radix-ui/react-accordion` from 1.2.16 to 1.2.17
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/accordion/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/accordion)

Updates `@radix-ui/react-alert-dialog` from 1.1.19 to 1.1.20
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/alert-dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/alert-dialog)

Updates `@radix-ui/react-checkbox` from 1.3.7 to 1.3.8
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/checkbox)

Updates `@radix-ui/react-label` from 2.1.11 to 2.1.12
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

Updates `@radix-ui/react-menubar` from 1.1.20 to 1.1.21
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/menubar/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/menubar)

Updates `@radix-ui/react-popover` from 1.1.19 to 1.1.20
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover)

Updates `@radix-ui/react-scroll-area` from 1.2.14 to 1.2.15
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/scroll-area)

Updates `@radix-ui/react-select` from 2.3.3 to 2.3.4
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select)

Updates `@radix-ui/react-slider` from 1.4.3 to 1.4.4
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slider/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slider)

Updates `@radix-ui/react-switch` from 1.3.3 to 1.3.4
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

Updates `@radix-ui/react-tabs` from 1.1.17 to 1.1.18
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs)

Updates `@radix-ui/react-toast` from 1.2.19 to 1.2.20
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toast/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toast)

Updates `@radix-ui/react-toggle-group` from 1.1.15 to 1.1.16
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toggle-group/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toggle-group)

Updates `@radix-ui/react-visually-hidden` from 1.2.7 to 1.2.8
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/visually-hidden/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/visually-hidden)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-accordion"
  dependency-version: 1.2.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-alert-dialog"
  dependency-version: 1.1.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-checkbox"
  dependency-version: 1.3.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-collapsible"
  dependency-version: 1.1.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-menubar"
  dependency-version: 1.1.21
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-popover"
  dependency-version: 1.1.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-scroll-area"
  dependency-version: 1.2.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-slider"
  dependency-version: 1.4.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-toast"
  dependency-version: 1.2.20
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-toggle-group"
  dependency-version: 1.1.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-visually-hidden"
  dependency-version: 1.2.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 14:44:17 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8670760390 chore(deps): bump tslog from 4.11.0 to 5.1.0 (#1328)
Bumps [tslog](https://github.com/fullstack-build/tslog) from 4.11.0 to 5.1.0.
- [Release notes](https://github.com/fullstack-build/tslog/releases)
- [Changelog](https://github.com/fullstack-build/tslog/blob/master/CHANGELOG.md)
- [Commits](https://github.com/fullstack-build/tslog/compare/v4.11.0...v5.1.0)

---
updated-dependencies:
- dependency-name: tslog
  dependency-version: 5.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-25 14:44:04 -04:00
Dan DitomasoandGitHub 54cffcf702 fix: use crypto.getRandomValues for randId to eliminate collision risk (#1282)
* fix(randId): use crypto.getRandomValues for connection IDs

Math.random() gives ~30 bits of entropy and no cryptographic guarantees.
Switch to crypto.getRandomValues over a Uint32 buffer so device / saved-
connection IDs get full 32-bit range and don't collide across concurrent
transports.

Closes #1273

* fix: assert Uint32Array index for strict TS7 compat

Under strict + noUncheckedIndexedAccess (TS7 default), buf[0] is
number | undefined. Uint32Array(1) guarantees length 1, so assert
non-null to satisfy the number return type.
2026-07-24 22:31:34 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
51b5e5c583 chore(deps): bump redhat-actions/push-to-registry from 2 to 3 (#1336)
Bumps [redhat-actions/push-to-registry](https://github.com/redhat-actions/push-to-registry) from 2 to 3.
- [Release notes](https://github.com/redhat-actions/push-to-registry/releases)
- [Changelog](https://github.com/redhat-actions/push-to-registry/blob/main/CHANGELOG.md)
- [Commits](https://github.com/redhat-actions/push-to-registry/compare/v2...v3)

---
updated-dependencies:
- dependency-name: redhat-actions/push-to-registry
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 22:31:17 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
01325180d3 chore(deps-dev): bump tsdown from 0.22.11 to 0.22.13 (#1333)
Bumps [tsdown](https://github.com/rolldown/tsdown) from 0.22.11 to 0.22.13.
- [Release notes](https://github.com/rolldown/tsdown/releases)
- [Commits](https://github.com/rolldown/tsdown/compare/v0.22.11...v0.22.13)

---
updated-dependencies:
- dependency-name: tsdown
  dependency-version: 0.22.12
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 22:18:17 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
40cc2f6296 chore(deps-dev): bump happy-dom from 20.10.6 to 20.11.0 (#1330)
Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 20.10.6 to 20.11.0.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.10.6...v20.11.0)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-version: 20.11.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 21:52:14 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
862c948472 chore(deps-dev): bump lint-staged from 17.0.8 to 17.1.0 (#1329)
Bumps [lint-staged](https://github.com/lint-staged/lint-staged) from 17.0.8 to 17.1.0.
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v17.0.8...v17.1.0)

---
updated-dependencies:
- dependency-name: lint-staged
  dependency-version: 17.1.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 21:37:25 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6046c0e47e chore(deps): bump react-hook-form from 7.81.0 to 7.82.0 (#1331)
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.81.0 to 7.82.0.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.81.0...v7.82.0)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.82.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 21:37:05 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6a0cfafd94 chore(deps): bump lucide-react from 1.24.0 to 1.25.0 (#1332)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 1.24.0 to 1.25.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.25.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 21:36:58 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d2e465cfa0 chore(deps-dev): bump @types/serviceworker from 0.0.199 to 0.0.200 (#1334)
Bumps [@types/serviceworker](https://github.com/microsoft/TypeScript-DOM-Lib-Generator) from 0.0.199 to 0.0.200.
- [Release notes](https://github.com/microsoft/TypeScript-DOM-Lib-Generator/releases)
- [Commits](https://github.com/microsoft/TypeScript-DOM-Lib-Generator/compare/@types/serviceworker@0.0.199...@types/serviceworker@0.0.200)

---
updated-dependencies:
- dependency-name: "@types/serviceworker"
  dependency-version: 0.0.200
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 21:36:43 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f7f382ce29 chore(deps): bump vite from 8.1.4 to 8.1.5 (#1335)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.1.4 to 8.1.5.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.1.5/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.1.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 21:36:36 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
77cfbf257d chore(deps): bump redhat-actions/buildah-build from 2 to 3 (#1337)
Bumps [redhat-actions/buildah-build](https://github.com/redhat-actions/buildah-build) from 2 to 3.
- [Release notes](https://github.com/redhat-actions/buildah-build/releases)
- [Changelog](https://github.com/redhat-actions/buildah-build/blob/main/CHANGELOG.md)
- [Commits](https://github.com/redhat-actions/buildah-build/compare/v2...v3)

---
updated-dependencies:
- dependency-name: redhat-actions/buildah-build
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-24 21:30:50 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Hunter Thornsberry
01785fc024 chore(deps): bump @tailwindcss/vite from 4.3.2 to 4.3.3 (#1316)
Bumps [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) from 4.3.2 to 4.3.3.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/packages/@tailwindcss-vite)

---
updated-dependencies:
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Hunter Thornsberry <hunter@hunterthornsberry.com>
2026-07-22 13:46:44 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Hunter Thornsberry
bfecd2a3f3 chore(deps-dev): bump autoprefixer from 10.5.2 to 10.5.4 (#1318)
Bumps [autoprefixer](https://github.com/postcss/autoprefixer) from 10.5.2 to 10.5.4.
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.5.2...10.5.4)

---
updated-dependencies:
- dependency-name: autoprefixer
  dependency-version: 10.5.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Hunter Thornsberry <hunter@hunterthornsberry.com>
2026-07-22 13:17:20 -04:00
Hunter ThornsberryandGitHub d49c80b829 Merge branch 'main' into dependabot/npm_and_yarn/tanstack-4e0cd3caa5 2026-07-22 12:51:32 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d707bb1674 chore(deps-dev): bump tsdown from 0.22.7 to 0.22.11 (#1315)
Bumps [tsdown](https://github.com/rolldown/tsdown) from 0.22.7 to 0.22.11.
- [Release notes](https://github.com/rolldown/tsdown/releases)
- [Commits](https://github.com/rolldown/tsdown/compare/v0.22.7...v0.22.11)

---
updated-dependencies:
- dependency-name: tsdown
  dependency-version: 0.22.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 12:49:01 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
20f81b2554 chore(deps): bump immer from 11.1.11 to 11.1.15 (#1317)
Bumps [immer](https://github.com/immerjs/immer) from 11.1.11 to 11.1.15.
- [Release notes](https://github.com/immerjs/immer/releases)
- [Commits](https://github.com/immerjs/immer/compare/v11.1.11...v11.1.15)

---
updated-dependencies:
- dependency-name: immer
  dependency-version: 11.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 12:48:29 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
03aa01e6e7 chore(deps-dev): bump @tailwindcss/postcss from 4.3.2 to 4.3.3 (#1319)
Bumps [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss) from 4.3.2 to 4.3.3.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/packages/@tailwindcss-postcss)

---
updated-dependencies:
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 12:45:28 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
214bb413bc chore(deps-dev): bump tailwindcss from 4.3.2 to 4.3.3 (#1320)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) from 4.3.2 to 4.3.3.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.3/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-version: 4.3.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 12:44:51 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3a1a29326d chore(deps): bump better-result from 2.9.2 to 2.10.0 (#1321)
Bumps [better-result](https://github.com/dmmulroy/better-result) from 2.9.2 to 2.10.0.
- [Release notes](https://github.com/dmmulroy/better-result/releases)
- [Commits](https://github.com/dmmulroy/better-result/compare/v2.9.2...v2.10.0)

---
updated-dependencies:
- dependency-name: better-result
  dependency-version: 2.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 12:44:22 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ebe2b32554 chore(deps): bump react-i18next from 17.0.9 to 17.0.10 (#1322)
Bumps [react-i18next](https://github.com/i18next/react-i18next) from 17.0.9 to 17.0.10.
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v17.0.9...v17.0.10)

---
updated-dependencies:
- dependency-name: react-i18next
  dependency-version: 17.0.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-22 12:43:57 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>Dan DitomasoHunter Thornsberry
aec3ad9ed3 chore(deps-dev): bump oxfmt from 0.58.0 to 0.59.0 (#1306)
* chore(deps-dev): bump oxfmt from 0.58.0 to 0.59.0

Bumps [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) from 0.58.0 to 0.59.0.
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.59.0/npm/oxfmt)

---
updated-dependencies:
- dependency-name: oxfmt
  dependency-version: 0.59.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore: fix formatting issues for oxfmt 0.59.0

* fix: add vitest.config.ts for packages/ui to avoid unplugin-dts TypeScript 7 crash

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Dan Ditomaso <dan.ditomaso@gmail.com>
Co-authored-by: Hunter Thornsberry <hunter@hunterthornsberry.com>
2026-07-22 12:43:26 -04:00
dependabot[bot]andGitHub d71be596ab chore(deps): bump the tanstack group across 1 directory with 3 updates
Bumps the tanstack group with 3 updates in the / directory: [@tanstack/react-router](https://github.com/TanStack/router/tree/HEAD/packages/react-router), [@tanstack/router-cli](https://github.com/TanStack/router/tree/HEAD/packages/router-cli) and [@tanstack/router-plugin](https://github.com/TanStack/router/tree/HEAD/packages/router-plugin).


Updates `@tanstack/react-router` from 1.170.17 to 1.170.18
- [Release notes](https://github.com/TanStack/router/releases)
- [Changelog](https://github.com/TanStack/router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/TanStack/router/commits/@tanstack/react-router@1.170.18/packages/react-router)

Updates `@tanstack/router-cli` from 1.167.18 to 1.167.21
- [Release notes](https://github.com/TanStack/router/releases)
- [Changelog](https://github.com/TanStack/router/blob/main/packages/router-cli/CHANGELOG.md)
- [Commits](https://github.com/TanStack/router/commits/@tanstack/router-cli@1.167.21/packages/router-cli)

Updates `@tanstack/router-plugin` from 1.168.19 to 1.168.22
- [Release notes](https://github.com/TanStack/router/releases)
- [Changelog](https://github.com/TanStack/router/blob/main/packages/router-plugin/CHANGELOG.md)
- [Commits](https://github.com/TanStack/router/commits/@tanstack/router-plugin@1.168.22/packages/router-plugin)

---
updated-dependencies:
- dependency-name: "@tanstack/react-router"
  dependency-version: 1.170.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: tanstack
- dependency-name: "@tanstack/router-cli"
  dependency-version: 1.167.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: tanstack
- dependency-name: "@tanstack/router-plugin"
  dependency-version: 1.168.20
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: tanstack
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-07-21 08:43:31 +00:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1eeabeaa91 chore(deps): bump actions/setup-python from 6 to 7 (#1323)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 6 to 7.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-20 21:33:40 -04:00
Dan DitomasoandGitHub 948474a18e fix(i18n): translate ErrorPage report intro (#1284)
The 'Please include the following information in your report:' string
was hardcoded in the component. The matching key 'errorPage.reportInstructions'
already exists in en/ui.json — wire it up so translators actually see it.

Closes #1274
2026-07-17 22:56:41 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Hunter275
abe54e3b31 chore(deps-dev): bump oxlint from 1.73.0 to 1.74.0 (#1307)
* chore(deps-dev): bump oxlint from 1.73.0 to 1.74.0

Bumps [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) from 1.73.0 to 1.74.0.
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.74.0/npm/oxlint)

---
updated-dependencies:
- dependency-name: oxlint
  dependency-version: 1.74.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* installed missing typescript packages

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Hunter275 <hunterthor275@gmail.com>
2026-07-17 22:11:22 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
56cfc31e0f chore(deps-dev): bump tsdown from 0.22.5 to 0.22.7 (#1302)
Bumps [tsdown](https://github.com/rolldown/tsdown) from 0.22.5 to 0.22.7.
- [Release notes](https://github.com/rolldown/tsdown/releases)
- [Commits](https://github.com/rolldown/tsdown/compare/v0.22.5...v0.22.7)

---
updated-dependencies:
- dependency-name: tsdown
  dependency-version: 0.22.7
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-16 21:31:44 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2f9effb04c chore(deps-dev): bump tar from 7.5.19 to 7.5.20 (#1300)
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.19 to 7.5.20.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.19...v7.5.20)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.20
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-15 08:13:07 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d6fcc211dc chore(deps-dev): bump tsdown from 0.22.4 to 0.22.5 (#1301)
Bumps [tsdown](https://github.com/rolldown/tsdown) from 0.22.4 to 0.22.5.
- [Release notes](https://github.com/rolldown/tsdown/releases)
- [Commits](https://github.com/rolldown/tsdown/compare/v0.22.4...v0.22.5)

---
updated-dependencies:
- dependency-name: tsdown
  dependency-version: 0.22.5
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-15 08:12:53 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
84948adfa9 chore(deps): bump actions/setup-node from 6 to 7 (#1299)
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 6 to 7.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-14 22:16:38 -04:00
Dan DitomasoandGitHub 8ffdcce1fa chore: remove production console noise (#1281)
Drop the debug log inside ChannelChat's MessageSkeleton (fired on every
Suspense fallback) and gate the dev-overrides log on isDev so it stops
running at module load in prod.

Closes #1275
Closes #1276
2026-07-13 21:42:26 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Dan Ditomaso
3297518ff4 chore(deps-dev): bump typescript from 6.0.3 to 7.0.2 (#1293)
Bumps [typescript](https://github.com/microsoft/TypeScript) from 6.0.3 to 7.0.2.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/commits)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 7.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dan Ditomaso <dan.ditomaso@gmail.com>
2026-07-13 21:14:58 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2233ab3383 chore(deps): bump tslog from 4.10.2 to 4.11.0 (#1285)
Bumps [tslog](https://github.com/fullstack-build/tslog) from 4.10.2 to 4.11.0.
- [Release notes](https://github.com/fullstack-build/tslog/releases)
- [Changelog](https://github.com/fullstack-build/tslog/blob/master/CHANGELOG.md)
- [Commits](https://github.com/fullstack-build/tslog/compare/v4.10.2...v4.11.0)

---
updated-dependencies:
- dependency-name: tslog
  dependency-version: 4.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-13 13:59:21 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d7dbc7e7d3 chore(deps): bump vite from 8.1.3 to 8.1.4 (#1291)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.1.3 to 8.1.4.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.1.4/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.1.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-13 13:51:16 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1c746f0a83 chore(deps): bump @preact/signals-core from 1.14.3 to 1.14.4 (#1295)
Bumps [@preact/signals-core](https://github.com/preactjs/signals) from 1.14.3 to 1.14.4.
- [Release notes](https://github.com/preactjs/signals/releases)
- [Commits](https://github.com/preactjs/signals/compare/@preact/signals-core@1.14.3...@preact/signals-core@1.14.4)

---
updated-dependencies:
- dependency-name: "@preact/signals-core"
  dependency-version: 1.14.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-13 13:33:37 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b14cbb2ea4 chore(deps-dev): bump tsdown from 0.22.3 to 0.22.4 (#1292)
Bumps [tsdown](https://github.com/rolldown/tsdown) from 0.22.3 to 0.22.4.
- [Release notes](https://github.com/rolldown/tsdown/releases)
- [Commits](https://github.com/rolldown/tsdown/compare/v0.22.3...v0.22.4)

---
updated-dependencies:
- dependency-name: tsdown
  dependency-version: 0.22.4
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-13 13:28:10 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ea77054d1b chore(deps): bump i18next from 26.3.4 to 26.3.6 (#1290)
Bumps [i18next](https://github.com/i18next/i18next) from 26.3.4 to 26.3.6.
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.3.4...v26.3.6)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 26.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-13 13:19:07 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1818383efd chore(deps-dev): bump oxfmt from 0.57.0 to 0.58.0 (#1296)
Bumps [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) from 0.57.0 to 0.58.0.
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.58.0/npm/oxfmt)

---
updated-dependencies:
- dependency-name: oxfmt
  dependency-version: 0.58.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-13 13:10:42 -04:00
AustinandGitHub 1248111621 Increase dependabot PR limit to 10 (#1294)
Defaults to 5. This big monorepo needs a bit more.
2026-07-13 13:04:55 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c4fd4e33ff chore(deps): bump idb-keyval from 6.2.6 to 6.3.0 (#1288)
Bumps [idb-keyval](https://github.com/jakearchibald/idb-keyval) from 6.2.6 to 6.3.0.
- [Changelog](https://github.com/jakearchibald/idb-keyval/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jakearchibald/idb-keyval/compare/v6.2.6...v6.3.0)

---
updated-dependencies:
- dependency-name: idb-keyval
  dependency-version: 6.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-13 12:54:27 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8189d6cf76 chore(deps): bump react-i18next from 17.0.8 to 17.0.9 (#1289)
Bumps [react-i18next](https://github.com/i18next/react-i18next) from 17.0.8 to 17.0.9.
- [Changelog](https://github.com/i18next/react-i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/react-i18next/compare/v17.0.8...v17.0.9)

---
updated-dependencies:
- dependency-name: react-i18next
  dependency-version: 17.0.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-13 12:46:24 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a7d5ea7e94 chore(deps): bump lucide-react from 1.23.0 to 1.24.0 (#1286)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 1.23.0 to 1.24.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.24.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-13 12:41:02 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
b900352920 chore(deps): bump immer from 11.1.8 to 11.1.11 (#1287)
Bumps [immer](https://github.com/immerjs/immer) from 11.1.8 to 11.1.11.
- [Release notes](https://github.com/immerjs/immer/releases)
- [Commits](https://github.com/immerjs/immer/compare/v11.1.8...v11.1.11)

---
updated-dependencies:
- dependency-name: immer
  dependency-version: 11.1.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-13 12:34:46 -04:00
b0bde79fc9 reoder the NavLinks (#1268)
Co-authored-by: Hunter275 <hunterthor275@gmail.com>
2026-07-11 22:29:32 -04:00
38c7569d0f Fix blank error message when using Serial on Firefox (#1254)
* add checks for both bluetooth and serial in the FeatureErrorMessage logic

* move the check further down and only show if neither needsSecureContext and needsFeature are empty

---------

Co-authored-by: Hunter275 <hunterthor275@gmail.com>
2026-07-10 21:10:39 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Hunter ThornsberryHunter275
2578415b83 chore(deps): bump @tailwindcss/vite from 4.3.1 to 4.3.2 (#1262)
* chore(deps): bump @tailwindcss/vite from 4.3.1 to 4.3.2

Bumps [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) from 4.3.1 to 4.3.2.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.2/packages/@tailwindcss-vite)

---
updated-dependencies:
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* bump vite version fix

* fix merge issue

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Hunter Thornsberry <hunter@hunterthornsberry.com>
Co-authored-by: Hunter275 <hunterthor275@gmail.com>
2026-07-10 19:00:57 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
625ee71e48 chore(deps): bump lucide-react from 1.22.0 to 1.23.0 (#1265)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 1.22.0 to 1.23.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.23.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.23.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 16:55:47 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
173ad48424 chore(deps): bump @types/node from 26.0.1 to 26.1.0 (#1264)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 26.0.1 to 26.1.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 15:21:00 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c6bdd3c8b2 chore(deps-dev): bump oxlint from 1.71.0 to 1.73.0 (#1263)
Bumps [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) from 1.71.0 to 1.73.0.
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.73.0/npm/oxlint)

---
updated-dependencies:
- dependency-name: oxlint
  dependency-version: 1.73.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 15:18:11 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
671aefd11c chore(deps): bump the radix-ui group across 1 directory with 19 updates (#1247)
---
updated-dependencies:
- dependency-name: "@radix-ui/react-accordion"
  dependency-version: 1.2.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-alert-dialog"
  dependency-version: 1.1.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-checkbox"
  dependency-version: 1.3.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-collapsible"
  dependency-version: 1.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-menubar"
  dependency-version: 1.1.19
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-popover"
  dependency-version: 1.1.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-scroll-area"
  dependency-version: 1.2.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-slider"
  dependency-version: 1.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-toast"
  dependency-version: 1.2.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-toggle-group"
  dependency-version: 1.1.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-visually-hidden"
  dependency-version: 1.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-10 15:14:34 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ce61f38254 chore(deps): bump react-hook-form from 7.80.0 to 7.81.0 (#1258)
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.80.0 to 7.81.0.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.80.0...v7.81.0)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.81.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 21:43:17 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4b4eb3265d chore(deps): bump the tanstack group across 1 directory with 3 updates (#1246)
Bumps the tanstack group with 3 updates in the / directory: [@tanstack/react-router](https://github.com/TanStack/router/tree/HEAD/packages/react-router), [@tanstack/router-cli](https://github.com/TanStack/router/tree/HEAD/packages/router-cli) and [@tanstack/router-plugin](https://github.com/TanStack/router/tree/HEAD/packages/router-plugin).


Updates `@tanstack/react-router` from 1.170.16 to 1.170.17
- [Release notes](https://github.com/TanStack/router/releases)
- [Changelog](https://github.com/TanStack/router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/TanStack/router/commits/@tanstack/react-router@1.170.17/packages/react-router)

Updates `@tanstack/router-cli` from 1.167.17 to 1.167.18
- [Release notes](https://github.com/TanStack/router/releases)
- [Changelog](https://github.com/TanStack/router/blob/main/packages/router-cli/CHANGELOG.md)
- [Commits](https://github.com/TanStack/router/commits/@tanstack/router-cli@1.167.18/packages/router-cli)

Updates `@tanstack/router-plugin` from 1.168.18 to 1.168.19
- [Release notes](https://github.com/TanStack/router/releases)
- [Changelog](https://github.com/TanStack/router/blob/main/packages/router-plugin/CHANGELOG.md)
- [Commits](https://github.com/TanStack/router/commits/@tanstack/router-plugin@1.168.19/packages/router-plugin)

---
updated-dependencies:
- dependency-name: "@tanstack/react-router"
  dependency-version: 1.170.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: tanstack
- dependency-name: "@tanstack/router-cli"
  dependency-version: 1.167.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: tanstack
- dependency-name: "@tanstack/router-plugin"
  dependency-version: 1.168.19
  dependency-type: direct:development
  update-type: version-update:semver-patch
  dependency-group: tanstack
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 21:31:27 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4452eae48f chore(deps): bump vite from 8.1.0 to 8.1.3 (#1259)
---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.1.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 20:03:49 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a7f7e8f93b chore(deps-dev): bump vitest from 4.1.9 to 4.1.10 (#1257)
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 4.1.9 to 4.1.10.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.10/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 4.1.10
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 16:39:42 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
69f9ad8bc3 chore(deps-dev): bump tailwindcss from 4.3.1 to 4.3.2 (#1249)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) from 4.3.1 to 4.3.2.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.2/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-version: 4.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 15:36:06 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
501388eace chore(deps-dev): bump @types/chrome from 0.2.0 to 0.2.2 (#1248)
Bumps [@types/chrome](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chrome) from 0.2.0 to 0.2.2.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/chrome)

---
updated-dependencies:
- dependency-name: "@types/chrome"
  dependency-version: 0.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 15:11:19 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ee174e7e5f chore(deps-dev): bump oxfmt from 0.56.0 to 0.57.0 (#1250)
Bumps [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) from 0.56.0 to 0.57.0.
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.57.0/npm/oxfmt)

---
updated-dependencies:
- dependency-name: oxfmt
  dependency-version: 0.57.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-09 15:05:27 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Dan Ditomaso
ec0c91a334 chore(deps-dev): bump @tailwindcss/postcss from 4.3.1 to 4.3.2 (#1236)
Bumps [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss) from 4.3.1 to 4.3.2.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.2/packages/@tailwindcss-postcss)

---
updated-dependencies:
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.3.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dan Ditomaso <dan.ditomaso@gmail.com>
2026-07-04 09:09:27 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
78066d545f chore(deps): bump idb-keyval from 6.2.5 to 6.2.6 (#1233)
Bumps [idb-keyval](https://github.com/jakearchibald/idb-keyval) from 6.2.5 to 6.2.6.
- [Changelog](https://github.com/jakearchibald/idb-keyval/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jakearchibald/idb-keyval/compare/v6.2.5...v6.2.6)

---
updated-dependencies:
- dependency-name: idb-keyval
  dependency-version: 6.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-04 09:01:08 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
dba3b9b10a chore(deps): bump @types/node from 25.9.3 to 26.0.1 (#1195)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 25.9.3 to 26.0.1.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 26.0.1
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-04 09:00:30 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
103082fb58 chore(deps-dev): bump @types/serviceworker from 0.0.197 to 0.0.199 (#1238)
Bumps [@types/serviceworker](https://github.com/microsoft/TypeScript-DOM-Lib-Generator) from 0.0.197 to 0.0.199.
- [Release notes](https://github.com/microsoft/TypeScript-DOM-Lib-Generator/releases)
- [Commits](https://github.com/microsoft/TypeScript-DOM-Lib-Generator/compare/@types/serviceworker@0.0.197...@types/serviceworker@0.0.199)

---
updated-dependencies:
- dependency-name: "@types/serviceworker"
  dependency-version: 0.0.199
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-04 08:59:36 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
858de11b3b chore(deps): bump i18next from 26.3.3 to 26.3.4 (#1237)
Bumps [i18next](https://github.com/i18next/i18next) from 26.3.3 to 26.3.4.
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.3.3...v26.3.4)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 26.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-04 08:59:11 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3ec58d8827 chore(deps): bump lucide-react from 1.21.0 to 1.22.0 (#1234)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 1.21.0 to 1.22.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.22.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.22.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-02 09:20:18 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
14ab1409e0 chore(deps): bump i18next from 26.3.2 to 26.3.3 (#1228)
Bumps [i18next](https://github.com/i18next/i18next) from 26.3.2 to 26.3.3.
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.3.2...v26.3.3)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 26.3.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-01 10:38:06 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
57158e4d63 chore(deps): bump vite from 8.0.16 to 8.1.0 (#1201)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 8.0.16 to 8.1.0.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/create-vite@8.1.0/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-01 10:37:44 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f07e04e512 chore(deps-dev): bump tar from 7.5.17 to 7.5.19 (#1230)
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.17 to 7.5.19.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.17...v7.5.19)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.19
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-01 09:47:57 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
fd0fcb12ab chore(deps): bump react-hook-form from 7.79.0 to 7.80.0 (#1226)
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.79.0 to 7.80.0.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.79.0...v7.80.0)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.80.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 14:05:40 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
bb09381ed7 chore(deps): bump @bufbuild/protobuf from 2.12.0 to 2.12.1 (#1221)
Bumps [@bufbuild/protobuf](https://github.com/bufbuild/protobuf-es/tree/HEAD/packages/protobuf) from 2.12.0 to 2.12.1.
- [Release notes](https://github.com/bufbuild/protobuf-es/releases)
- [Commits](https://github.com/bufbuild/protobuf-es/commits/v2.12.1/packages/protobuf)

---
updated-dependencies:
- dependency-name: "@bufbuild/protobuf"
  dependency-version: 2.12.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 13:12:31 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e884514637 chore(deps-dev): bump @playwright/test from 1.60.0 to 1.61.1 (#1225)
Bumps [@playwright/test](https://github.com/microsoft/playwright) from 1.60.0 to 1.61.1.
- [Release notes](https://github.com/microsoft/playwright/releases)
- [Commits](https://github.com/microsoft/playwright/compare/v1.60.0...v1.61.1)

---
updated-dependencies:
- dependency-name: "@playwright/test"
  dependency-version: 1.61.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 13:04:10 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
da51652ac1 chore(deps-dev): bump gzipper from 8.2.1 to 8.3.0 (#1219)
Bumps [gzipper](https://github.com/gios/gzipper) from 8.2.1 to 8.3.0.
- [Release notes](https://github.com/gios/gzipper/releases)
- [Changelog](https://github.com/gios/gzipper/blob/main/CHANGELOG.md)
- [Commits](https://github.com/gios/gzipper/compare/v8.2.1...v8.3.0)

---
updated-dependencies:
- dependency-name: gzipper
  dependency-version: 8.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 12:24:49 -04:00
AustinandGitHub c4f48a5ce6 chore(deps): update packageManager to pnpm@11.9.0 (#1224) 2026-06-28 12:08:02 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
16e6ac6ef9 chore(deps): bump the radix-ui group with 3 updates (#1223)
Bumps the radix-ui group with 3 updates: [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label), [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) and [@radix-ui/react-toast](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toast).


Updates `@radix-ui/react-label` from 2.1.9 to 2.1.10
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

Updates `@radix-ui/react-switch` from 1.3.0 to 1.3.1
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

Updates `@radix-ui/react-toast` from 1.2.16 to 1.2.17
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toast/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toast)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
- dependency-name: "@radix-ui/react-toast"
  dependency-version: 1.2.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: radix-ui
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 12:00:53 -04:00
AustinandGitHub 0ef3b1cbe6 dependabot: Group radix-ui (#1222)
These projects all depend on each other and make a real mess. Group them.
2026-06-28 11:50:50 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
822e8e06c9 chore(deps-dev): bump @types/chrome from 0.1.43 to 0.2.0 (#1209)
Bumps [@types/chrome](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chrome) from 0.1.43 to 0.2.0.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/chrome)

---
updated-dependencies:
- dependency-name: "@types/chrome"
  dependency-version: 0.2.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 11:43:01 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1ee012b012 chore(deps): bump @radix-ui/react-select from 2.3.0 to 2.3.1 (#1218)
Bumps [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) from 2.3.0 to 2.3.1.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 11:37:12 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
80e876ab9c chore(deps): bump i18next from 26.3.1 to 26.3.2 (#1217)
Bumps [i18next](https://github.com/i18next/i18next) from 26.3.1 to 26.3.2.
- [Release notes](https://github.com/i18next/i18next/releases)
- [Changelog](https://github.com/i18next/i18next/blob/master/CHANGELOG.md)
- [Commits](https://github.com/i18next/i18next/compare/v26.3.1...v26.3.2)

---
updated-dependencies:
- dependency-name: i18next
  dependency-version: 26.3.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 11:31:51 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c4f274c8ce chore(deps): bump @radix-ui/react-checkbox from 1.3.4 to 1.3.5 (#1216)
Bumps [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) from 1.3.4 to 1.3.5.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/checkbox)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-checkbox"
  dependency-version: 1.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 11:25:19 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f8ae859614 chore(deps): bump @preact/signals-core from 1.14.2 to 1.14.3 (#1214)
Bumps [@preact/signals-core](https://github.com/preactjs/signals) from 1.14.2 to 1.14.3.
- [Release notes](https://github.com/preactjs/signals/releases)
- [Commits](https://github.com/preactjs/signals/compare/@preact/signals-core@1.14.2...@preact/signals-core@1.14.3)

---
updated-dependencies:
- dependency-name: "@preact/signals-core"
  dependency-version: 1.14.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 11:19:03 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
02b011351c chore(deps-dev): bump oxlint from 1.70.0 to 1.71.0 (#1215)
Bumps [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) from 1.70.0 to 1.71.0.
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.71.0/npm/oxlint)

---
updated-dependencies:
- dependency-name: oxlint
  dependency-version: 1.71.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 11:13:43 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d9750a2f65 chore(deps-dev): bump oxfmt from 0.55.0 to 0.56.0 (#1213)
Bumps [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) from 0.55.0 to 0.56.0.
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.56.0/npm/oxfmt)

---
updated-dependencies:
- dependency-name: oxfmt
  dependency-version: 0.56.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 11:07:23 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
cc912172d9 chore(deps-dev): bump tar from 7.5.16 to 7.5.17 (#1212)
Bumps [tar](https://github.com/isaacs/node-tar) from 7.5.16 to 7.5.17.
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.16...v7.5.17)

---
updated-dependencies:
- dependency-name: tar
  dependency-version: 7.5.17
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 11:01:53 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c94a53dde9 chore(deps): bump @radix-ui/react-slider from 1.4.0 to 1.4.1 (#1211)
Bumps [@radix-ui/react-slider](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slider) from 1.4.0 to 1.4.1.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slider/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slider)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-slider"
  dependency-version: 1.4.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 11:00:48 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
848c840008 chore(deps): bump @radix-ui/react-scroll-area from 1.2.11 to 1.2.12 (#1210)
Bumps [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) from 1.2.11 to 1.2.12.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/scroll-area)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-scroll-area"
  dependency-version: 1.2.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 10:55:23 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d2475f99e8 chore(deps-dev): bump autoprefixer from 10.5.0 to 10.5.2 (#1207)
Bumps [autoprefixer](https://github.com/postcss/autoprefixer) from 10.5.0 to 10.5.2.
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.5.0...10.5.2)

---
updated-dependencies:
- dependency-name: autoprefixer
  dependency-version: 10.5.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 10:48:37 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c57b3dc1c1 chore(deps): bump @radix-ui/react-tooltip from 1.2.9 to 1.2.10 (#1208)
Bumps [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) from 1.2.9 to 1.2.10.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 10:41:22 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
454adaa5d6 chore(deps-dev): bump @bufbuild/protoc-gen-es from 2.12.0 to 2.12.1 (#1204)
Bumps [@bufbuild/protoc-gen-es](https://github.com/bufbuild/protobuf-es/tree/HEAD/packages/protoc-gen-es) from 2.12.0 to 2.12.1.
- [Release notes](https://github.com/bufbuild/protobuf-es/releases)
- [Commits](https://github.com/bufbuild/protobuf-es/commits/v2.12.1/packages/protoc-gen-es)

---
updated-dependencies:
- dependency-name: "@bufbuild/protoc-gen-es"
  dependency-version: 2.12.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 10:36:10 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
86652bd45f chore(deps-dev): bump @vitejs/plugin-react from 6.0.2 to 6.0.3 (#1206)
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 6.0.2 to 6.0.3.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.3/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 10:04:01 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6bbc46a3af chore(deps): bump @radix-ui/react-menubar from 1.1.17 to 1.1.18 (#1205)
Bumps [@radix-ui/react-menubar](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/menubar) from 1.1.17 to 1.1.18.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/menubar/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/menubar)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-menubar"
  dependency-version: 1.1.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 10:02:20 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2042299fa8 chore(deps-dev): bump vite-plugin-dts from 5.0.2 to 5.0.3 (#1200)
Bumps [vite-plugin-dts](https://github.com/qmhc/unplugin-dts/tree/HEAD/packages/vite-plugin-dts) from 5.0.2 to 5.0.3.
- [Release notes](https://github.com/qmhc/unplugin-dts/releases)
- [Changelog](https://github.com/qmhc/unplugin-dts/blob/main/packages/vite-plugin-dts/CHANGELOG.md)
- [Commits](https://github.com/qmhc/unplugin-dts/commits/vite-plugin-dts@5.0.3/packages/vite-plugin-dts)

---
updated-dependencies:
- dependency-name: vite-plugin-dts
  dependency-version: 5.0.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 09:48:09 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
264eb48756 chore(deps): bump @radix-ui/react-dropdown-menu from 2.1.17 to 2.1.18 (#1203)
Bumps [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) from 2.1.17 to 2.1.18.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 09:47:22 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ccd9d552f1 chore(deps): bump @radix-ui/react-accordion from 1.2.13 to 1.2.14 (#1202)
Bumps [@radix-ui/react-accordion](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/accordion) from 1.2.13 to 1.2.14.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/accordion/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/accordion)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-accordion"
  dependency-version: 1.2.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 09:40:51 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
16cb8b396c chore(deps): bump lucide-react from 1.18.0 to 1.21.0 (#1196)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 1.18.0 to 1.21.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.21.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.21.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 09:30:58 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
13171616be chore(deps): bump @radix-ui/react-slot from 1.2.5 to 1.3.0 (#1199)
Bumps [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) from 1.2.5 to 1.3.0.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 09:26:33 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
98b8cfafe7 chore(deps): bump @radix-ui/react-popover from 1.1.16 to 1.1.17 (#1198)
Bumps [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) from 1.1.16 to 1.1.17.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-popover"
  dependency-version: 1.1.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 09:14:09 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
af0b50c070 chore(deps): bump @tanstack/react-router in the tanstack group (#1194)
Bumps the tanstack group with 1 update: [@tanstack/react-router](https://github.com/TanStack/router/tree/HEAD/packages/react-router).


Updates `@tanstack/react-router` from 1.170.15 to 1.170.16
- [Release notes](https://github.com/TanStack/router/releases)
- [Changelog](https://github.com/TanStack/router/blob/main/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/TanStack/router/commits/@tanstack/react-router@1.170.16/packages/react-router)

---
updated-dependencies:
- dependency-name: "@tanstack/react-router"
  dependency-version: 1.170.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: tanstack
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 09:11:19 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
91267d99e2 chore(deps-dev): bump lint-staged from 17.0.7 to 17.0.8 (#1197)
Bumps [lint-staged](https://github.com/lint-staged/lint-staged) from 17.0.7 to 17.0.8.
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v17.0.7...v17.0.8)

---
updated-dependencies:
- dependency-name: lint-staged
  dependency-version: 17.0.8
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-28 09:03:59 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
361323c490 chore(deps-dev): bump happy-dom from 20.10.3 to 20.10.6 (#1176)
Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 20.10.3 to 20.10.6.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.10.3...v20.10.6)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-version: 20.10.6
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 10:16:17 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
78476b4d47 chore(deps-dev): bump tsdown from 0.22.2 to 0.22.3 (#1185)
Bumps [tsdown](https://github.com/rolldown/tsdown) from 0.22.2 to 0.22.3.
- [Release notes](https://github.com/rolldown/tsdown/releases)
- [Commits](https://github.com/rolldown/tsdown/compare/v0.22.2...v0.22.3)

---
updated-dependencies:
- dependency-name: tsdown
  dependency-version: 0.22.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-23 07:59:10 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2fd53e3fd1 chore(deps): bump @radix-ui/react-separator from 1.1.9 to 1.1.10 (#1186)
Bumps [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) from 1.1.9 to 1.1.10.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 19:52:55 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5416282788 chore(deps): bump @radix-ui/react-dialog from 1.1.16 to 1.1.17 (#1178)
Bumps [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) from 1.1.16 to 1.1.17.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 17:39:29 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a472d8762b chore(deps): bump @radix-ui/react-visually-hidden from 1.2.5 to 1.2.6 (#1177)
Bumps [@radix-ui/react-visually-hidden](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/visually-hidden) from 1.2.5 to 1.2.6.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/visually-hidden/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/visually-hidden)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-visually-hidden"
  dependency-version: 1.2.6
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 17:16:03 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5d128140cb chore(deps): bump @radix-ui/react-tabs from 1.1.14 to 1.1.15 (#1184)
Bumps [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) from 1.1.14 to 1.1.15.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.15
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 17:08:15 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4acdd127f5 chore(deps): bump actions/upload-artifact from 4 to 7 (#1181)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 17:07:53 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9d1a408123 chore(deps): bump actions/checkout from 4 to 7 (#1182)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 17:07:01 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Austin
139ca7125f chore(deps): bump actions/setup-node from 4 to 6 (#1180)
* chore(deps): bump actions/setup-node from 4 to 6

Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Use Node 24

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Austin <vidplace7@gmail.com>
2026-06-22 17:06:30 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Austin
38755cd861 chore(deps): bump pnpm/action-setup from 4 to 6 (#1183)
* chore(deps): bump pnpm/action-setup from 4 to 6

Bumps [pnpm/action-setup](https://github.com/pnpm/action-setup) from 4 to 6.
- [Release notes](https://github.com/pnpm/action-setup/releases)
- [Commits](https://github.com/pnpm/action-setup/compare/v4...v6)

---
updated-dependencies:
- dependency-name: pnpm/action-setup
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* Remove pnpm version specifier (redundant)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Austin <vidplace7@gmail.com>
2026-06-22 17:01:40 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
25a0632f77 chore(deps): bump actions/setup-python from 5 to 6 (#1179)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-22 16:59:37 -04:00
ad692eef74 test(e2e): real-device Playwright messaging suite (#1121)
* fix(connections): connect to the just-added connection via the live store

addConnectionAndConnect() adds a connection and then connects to it in the
same tick, but connect() resolved the id against the memoized `connections`
closure, which is stale until the hook re-renders. The just-added id was
therefore reported as an unknown connection id and Save silently never
connected any HTTP/Serial/Bluetooth device. Read savedConnections from
useDeviceStore.getState() so the lookup always sees the live store.

* test(e2e): real-device Playwright messaging suite

Drives the actual web app in Chromium against real meshtasticd firmware over
the HTTP phone API and verifies text messaging in both directions across a
two-node mesh. Nodes mesh over the firmware's built-in UDP multicast
(224.0.0.69) with no MQTT/relay; distinct node numbers, real encryption.

- Default backend: two Docker meshtasticd sim nodes (daily-debian). The same
  specs run against physical hardware via E2E_DEVICE_MODE=hardware.
- An off-browser Python meshtastic peer (e2e/peer/peer.py) drives/asserts the
  non-browser node over the TCP phone API, mirroring firmware mcp-server tests.
- Coverage: connect over HTTPS, mesh->web receive, web->mesh send. Direct
  messages are fixme'd (see below). CI workflow runs it on Linux.

Bugs surfaced by the suite:
- Fixed (prior commit): connect-on-save never connected (stale-closure id
  lookup in useConnections).
- Not fixed: apps/web/src/core/subscriptions.ts throws 'ReferenceError:
  nodeDB is not defined' on every device-metrics telemetry packet (the #1050
  migration removed that store); caught per-packet, so messaging still works.
- Not fixed: direct messages are blocked by a PKI 'Keys Mismatch' (the SDK's
  stored peer public key != the key presented during NodeInfo exchange), seen
  even with fresh sim nodes.

* test(e2e): address Copilot review feedback

- waitForTcp(): destroy the probe socket on the error path so repeated
  connection failures don't accumulate sockets/FDs across the retry loop.
- Don't remove the mesh containers in Playwright globalTeardown in CI — it
  raced the workflow's failure log capture. Teardown is now gated on
  E2E_DOCKER_DOWN only; CI dumps device logs on failure and tears the mesh
  down in a final always() workflow step.

* fix(sdk): fold device-metrics telemetry into nodes

apps/web/src/core/subscriptions.ts called nodeDB.addDeviceMetrics() on every
device-metrics telemetry packet, but the #1050 migration removed that store —
so it threw 'ReferenceError: nodeDB is not defined' on each telemetry packet
(caught per-packet by the SDK's HandleFromRadio, so messaging still worked but
the error spammed the console).

Route device metrics into the SDK NodesClient via onTelemetryPacket instead —
mirroring the existing position handler; the Node domain already carries a
deviceMetrics field — and drop the dead app-side handler. Adds a NodesClient
test covering the fold.

* docs(e2e): accurate DM root cause + bug status

The direct-message fixme is a simulator limitation, not a web-app bug: the
keyless meshtasticd sim nodes NAK a DM with NO_CHANNEL (routing error 6) — no
Curve25519 keypair is provisioned/shared, and current firmware can't deliver a
direct message without a per-node key / decryptable channel. The app surfaces
this correctly (key-refresh dialog). Re-enable against hardware or once the sim
provisions keys.

Also: mark the nodeDB telemetry bug fixed and note the CI teardown change.

* docs(e2e): precise DM root cause (firmware/sim PKI)

Followed up on the suggestion to provision keys in config.security: the keys
ARE settable and persist (verified via admin), but on the native meshtasticd
sim they don't sync to the node's owner / NodeInfo key — owner.public_key stays
empty and the node keeps its MAC-derived num — so the two nodes never exchange
keys. Combined with the firmware refusing non-PKI DMs ('Unknown public key for
destination ... refusing to send legacy DM'), the DM is NAK'd with NO_CHANNEL.
A firmware/sim limitation; DMs work on real hardware. Spec stays fixme.

* docs(e2e): definitive DM root cause (SimRadio PKC payload limit)

Per the steer to research the firmware: PKI keygen is gated on a set LoRa
region (NodeDB.cpp:3051) and the sim boots region-UNSET — setting lora.region
via admin DOES make the nodes generate and exchange keys (verified both ways).
But a PKI-encrypted DM still can't traverse the SimRadio: the PKC overhead
exceeds its payload limit ('Payload size larger than compressed message allows!
Send empty payload'), so the packet is truncated and the receiver NAKs
NO_CHANNEL ('No suitable channel found for decoding, hash 0x0'). The firmware
skips PKC under --sim (Router.cpp:730) for exactly this reason, but --sim also
disables the config-file loading the web app needs, so they're mutually
exclusive. DMs work on real hardware; spec stays fixme with this detail.

---------

Co-authored-by: Dan Ditomaso <dan.ditomaso@gmail.com>
2026-06-20 11:19:50 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a7aa25aa30 chore(deps-dev): bump oxfmt from 0.54.0 to 0.55.0 (#1170)
Bumps [oxfmt](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxfmt) from 0.54.0 to 0.55.0.
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxfmt/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxfmt_v0.55.0/npm/oxfmt)

---
updated-dependencies:
- dependency-name: oxfmt
  dependency-version: 0.55.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 22:58:12 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
77cc20635b chore(deps): bump actions/checkout from 6 to 7 (#1169)
Bumps [actions/checkout](https://github.com/actions/checkout) from 6 to 7.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v6...v7)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 22:57:52 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
90b2cd1fc5 chore(deps): bump @radix-ui/react-toggle-group from 1.1.12 to 1.1.13 (#1168)
Bumps [@radix-ui/react-toggle-group](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toggle-group) from 1.1.12 to 1.1.13.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toggle-group/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toggle-group)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-toggle-group"
  dependency-version: 1.1.13
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 22:57:23 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a5e690e277 chore(deps-dev): bump oxlint from 1.69.0 to 1.70.0 (#1171)
Bumps [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) from 1.69.0 to 1.70.0.
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.70.0/npm/oxlint)

---
updated-dependencies:
- dependency-name: oxlint
  dependency-version: 1.70.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 22:21:22 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5ab71fcd4c chore(deps): bump @radix-ui/react-collapsible from 1.1.13 to 1.1.14 (#1172)
Bumps [@radix-ui/react-collapsible](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/collapsible) from 1.1.13 to 1.1.14.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/collapsible/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/collapsible)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-collapsible"
  dependency-version: 1.1.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 22:20:49 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
bd27ed93e9 chore(deps): bump @radix-ui/react-alert-dialog from 1.1.16 to 1.1.17 (#1173)
Bumps [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/alert-dialog) from 1.1.16 to 1.1.17.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/alert-dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/alert-dialog)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-alert-dialog"
  dependency-version: 1.1.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-19 22:20:26 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
16ec124fc3 chore(deps-dev): bump vitest from 4.1.8 to 4.1.9 (#1167)
Bumps [vitest](https://github.com/vitest-dev/vitest/tree/HEAD/packages/vitest) from 4.1.8 to 4.1.9.
- [Release notes](https://github.com/vitest-dev/vitest/releases)
- [Changelog](https://github.com/vitest-dev/vitest/blob/main/docs/releases.md)
- [Commits](https://github.com/vitest-dev/vitest/commits/v4.1.9/packages/vitest)

---
updated-dependencies:
- dependency-name: vitest
  dependency-version: 4.1.9
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 18:18:09 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c2839d04e3 chore(deps-dev): bump @bufbuild/protoc-gen-es from 1.10.1 to 2.12.0 (#1109)
Bumps [@bufbuild/protoc-gen-es](https://github.com/bufbuild/protobuf-es/tree/HEAD/packages/protoc-gen-es) from 1.10.1 to 2.12.0.
- [Release notes](https://github.com/bufbuild/protobuf-es/releases)
- [Commits](https://github.com/bufbuild/protobuf-es/commits/v2.12.0/packages/protoc-gen-es)

---
updated-dependencies:
- dependency-name: "@bufbuild/protoc-gen-es"
  dependency-version: 2.12.0
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-18 18:17:55 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Dan Ditomaso
251c073a42 chore(deps): bump @noble/curves from 1.9.7 to 2.2.0 (#1152)
* chore(deps): bump @noble/curves from 1.9.7 to 2.2.0

Bumps [@noble/curves](https://github.com/paulmillr/noble-curves) from 1.9.7 to 2.2.0.
- [Release notes](https://github.com/paulmillr/noble-curves/releases)
- [Commits](https://github.com/paulmillr/noble-curves/compare/1.9.7...2.2.0)

---
updated-dependencies:
- dependency-name: "@noble/curves"
  dependency-version: 2.2.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* refactor(web): migrate @noble/curves imports to v2 paths

- Move numberToHexUnpadded import from @noble/curves/abstract/utils to
  @noble/curves/utils.js (abstract/utils subpath removed in v2).
- Update @noble/curves/ed25519 to @noble/curves/ed25519.js to match
  required sub-import file extensions.
- Rename x25519.utils.randomPrivateKey() to randomSecretKey() (v2 API).

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dan Ditomaso <dan.ditomaso@gmail.com>
2026-06-16 22:12:32 -04:00
AustinandGitHub d2cb51d489 Execute oxfmt (#1166)
Ran
pnpm oxfmt .

This should get the web repo aligned so that we can better enforce oxfmt going forward.
2026-06-16 20:30:08 -04:00
AustinandGitHub c5690d1edb Update oxfmt and do not fail Actions (#1145)
* Update oxfmt to 0.54.0

* Add global oxfmtrc rules

* CI: Do not fail for formatting
2026-06-16 15:33:57 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e1ea1c4f02 chore(deps-dev): bump happy-dom from 20.8.9 to 20.10.3 (#1165)
Bumps [happy-dom](https://github.com/capricorn86/happy-dom) from 20.8.9 to 20.10.3.
- [Release notes](https://github.com/capricorn86/happy-dom/releases)
- [Commits](https://github.com/capricorn86/happy-dom/compare/v20.8.9...v20.10.3)

---
updated-dependencies:
- dependency-name: happy-dom
  dependency-version: 20.10.3
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 15:05:10 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
12c27092be chore(deps): bump @types/node from 24.12.0 to 25.9.3 (#1163)
Bumps [@types/node](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/node) from 24.12.0 to 25.9.3.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/node)

---
updated-dependencies:
- dependency-name: "@types/node"
  dependency-version: 25.9.3
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 15:00:06 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c3b9443701 chore(deps): bump @radix-ui/react-slot from 1.2.4 to 1.2.5 (#1164)
Bumps [@radix-ui/react-slot](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slot) from 1.2.4 to 1.2.5.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slot/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slot)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-slot"
  dependency-version: 1.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 14:56:10 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
0d89b09b1f chore(deps): bump react-map-gl from 8.1.0 to 8.1.1 (#1162)
Bumps [react-map-gl](https://github.com/visgl/react-map-gl) from 8.1.0 to 8.1.1.
- [Release notes](https://github.com/visgl/react-map-gl/releases)
- [Changelog](https://github.com/visgl/react-map-gl/blob/v8.1.1/CHANGELOG.md)
- [Commits](https://github.com/visgl/react-map-gl/compare/v8.1.0...v8.1.1)

---
updated-dependencies:
- dependency-name: react-map-gl
  dependency-version: 8.1.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 14:54:08 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
77206a19ac chore(deps): bump react-error-boundary from 6.1.1 to 6.1.2 (#1161)
Bumps [react-error-boundary](https://github.com/bvaughn/react-error-boundary) from 6.1.1 to 6.1.2.
- [Release notes](https://github.com/bvaughn/react-error-boundary/releases)
- [Commits](https://github.com/bvaughn/react-error-boundary/compare/6.1.1...6.1.2)

---
updated-dependencies:
- dependency-name: react-error-boundary
  dependency-version: 6.1.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 14:50:03 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
89bfb14c77 chore(deps): bump @hookform/resolvers from 5.2.2 to 5.4.0 (#1158)
Bumps [@hookform/resolvers](https://github.com/react-hook-form/resolvers) from 5.2.2 to 5.4.0.
- [Release notes](https://github.com/react-hook-form/resolvers/releases)
- [Commits](https://github.com/react-hook-form/resolvers/compare/v5.2.2...v5.4.0)

---
updated-dependencies:
- dependency-name: "@hookform/resolvers"
  dependency-version: 5.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 14:48:53 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
4754504191 chore(deps): bump react-hook-form from 7.78.0 to 7.79.0 (#1160)
Bumps [react-hook-form](https://github.com/react-hook-form/react-hook-form) from 7.78.0 to 7.79.0.
- [Release notes](https://github.com/react-hook-form/react-hook-form/releases)
- [Changelog](https://github.com/react-hook-form/react-hook-form/blob/master/CHANGELOG.md)
- [Commits](https://github.com/react-hook-form/react-hook-form/compare/v7.78.0...v7.79.0)

---
updated-dependencies:
- dependency-name: react-hook-form
  dependency-version: 7.79.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 14:45:09 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
69acc29a95 chore(deps): bump @radix-ui/react-tabs from 1.1.13 to 1.1.14 (#1159)
Bumps [@radix-ui/react-tabs](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tabs) from 1.1.13 to 1.1.14.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tabs/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tabs)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-tabs"
  dependency-version: 1.1.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 14:42:45 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2dc5dcde5c chore(deps-dev): bump @vitejs/plugin-basic-ssl from 2.1.4 to 2.3.0 (#1156)
Bumps [@vitejs/plugin-basic-ssl](https://github.com/vitejs/vite-plugin-basic-ssl) from 2.1.4 to 2.3.0.
- [Release notes](https://github.com/vitejs/vite-plugin-basic-ssl/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-basic-ssl/blob/main/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-basic-ssl/compare/v2.1.4...v2.3.0)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-basic-ssl"
  dependency-version: 2.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 14:38:17 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7809758537 chore(deps): bump @radix-ui/react-separator from 1.1.8 to 1.1.9 (#1157)
Bumps [@radix-ui/react-separator](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/separator) from 1.1.8 to 1.1.9.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/separator/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/separator)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-separator"
  dependency-version: 1.1.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 14:37:25 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f32b857705 chore(deps): bump idb-keyval from 6.2.2 to 6.2.5 (#1155)
Bumps [idb-keyval](https://github.com/jakearchibald/idb-keyval) from 6.2.2 to 6.2.5.
- [Changelog](https://github.com/jakearchibald/idb-keyval/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jakearchibald/idb-keyval/compare/v6.2.2...v6.2.5)

---
updated-dependencies:
- dependency-name: idb-keyval
  dependency-version: 6.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 14:30:11 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
21040e8876 chore(deps-dev): bump @types/serviceworker from 0.0.158 to 0.0.197 (#1154)
Bumps [@types/serviceworker](https://github.com/microsoft/TypeScript-DOM-Lib-Generator) from 0.0.158 to 0.0.197.
- [Release notes](https://github.com/microsoft/TypeScript-DOM-Lib-Generator/releases)
- [Commits](https://github.com/microsoft/TypeScript-DOM-Lib-Generator/compare/@types/serviceworker@0.0.158...@types/serviceworker@0.0.197)

---
updated-dependencies:
- dependency-name: "@types/serviceworker"
  dependency-version: 0.0.197
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 14:29:08 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
193f45d568 chore(deps): bump @radix-ui/react-popover from 1.1.15 to 1.1.16 (#1153)
Bumps [@radix-ui/react-popover](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/popover) from 1.1.15 to 1.1.16.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/popover/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/popover)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-popover"
  dependency-version: 1.1.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 14:28:15 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e6d25f2fd1 chore(deps): bump better-result from 1.0.1 to 2.9.2 (#1150)
Bumps [better-result](https://github.com/dmmulroy/better-result) from 1.0.1 to 2.9.2.
- [Release notes](https://github.com/dmmulroy/better-result/releases)
- [Commits](https://github.com/dmmulroy/better-result/compare/v1.0.1...v2.9.2)

---
updated-dependencies:
- dependency-name: better-result
  dependency-version: 2.9.2
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 14:21:43 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a8fcd81c2b chore(deps-dev): bump jsdom from 25.0.1 to 29.1.1 (#1149)
Bumps [jsdom](https://github.com/jsdom/jsdom) from 25.0.1 to 29.1.1.
- [Release notes](https://github.com/jsdom/jsdom/releases)
- [Commits](https://github.com/jsdom/jsdom/compare/v25.0.1...v29.1.1)

---
updated-dependencies:
- dependency-name: jsdom
  dependency-version: 29.1.1
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 14:20:39 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
039df71307 chore(deps-dev): bump tsdown from 0.15.12 to 0.22.2 (#1151)
Bumps [tsdown](https://github.com/rolldown/tsdown) from 0.15.12 to 0.22.2.
- [Release notes](https://github.com/rolldown/tsdown/releases)
- [Commits](https://github.com/rolldown/tsdown/compare/v0.15.12...v0.22.2)

---
updated-dependencies:
- dependency-name: tsdown
  dependency-version: 0.22.2
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 14:15:39 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3df59aa3a1 chore(deps): bump @radix-ui/react-alert-dialog from 1.1.15 to 1.1.16 (#1148)
Bumps [@radix-ui/react-alert-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/alert-dialog) from 1.1.15 to 1.1.16.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/alert-dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/alert-dialog)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-alert-dialog"
  dependency-version: 1.1.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 12:01:12 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3212026a89 chore(deps): bump js-cookie from 3.0.7 to 3.0.8 (#1147)
Bumps [js-cookie](https://github.com/js-cookie/js-cookie) from 3.0.7 to 3.0.8.
- [Release notes](https://github.com/js-cookie/js-cookie/releases)
- [Commits](https://github.com/js-cookie/js-cookie/compare/v3.0.7...v3.0.8)

---
updated-dependencies:
- dependency-name: js-cookie
  dependency-version: 3.0.8
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 11:57:29 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3fcba32b38 chore(deps): bump @turf/turf from 7.3.4 to 7.3.5 (#1146)
Bumps [@turf/turf](https://github.com/Turfjs/turf) from 7.3.4 to 7.3.5.
- [Release notes](https://github.com/Turfjs/turf/releases)
- [Changelog](https://github.com/Turfjs/turf/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Turfjs/turf/compare/v7.3.4...v7.3.5)

---
updated-dependencies:
- dependency-name: "@turf/turf"
  dependency-version: 7.3.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 11:53:18 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1095e1c5c4 chore(deps-dev): bump vite-plugin-dts from 4.5.4 to 5.0.2 (#1137)
Bumps [vite-plugin-dts](https://github.com/qmhc/unplugin-dts/tree/HEAD/packages/vite-plugin-dts) from 4.5.4 to 5.0.2.
- [Release notes](https://github.com/qmhc/unplugin-dts/releases)
- [Changelog](https://github.com/qmhc/unplugin-dts/blob/main/packages/vite-plugin-dts/CHANGELOG.md)
- [Commits](https://github.com/qmhc/unplugin-dts/commits/vite-plugin-dts@5.0.2/packages/vite-plugin-dts)

---
updated-dependencies:
- dependency-name: vite-plugin-dts
  dependency-version: 5.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 11:48:58 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6f4ad809ca chore(deps): bump sqlocal from 0.14.2 to 0.18.0 (#1144)
Bumps [sqlocal](https://github.com/DallasHoff/sqlocal) from 0.14.2 to 0.18.0.
- [Release notes](https://github.com/DallasHoff/sqlocal/releases)
- [Commits](https://github.com/DallasHoff/sqlocal/compare/v0.14.2...v0.18.0)

---
updated-dependencies:
- dependency-name: sqlocal
  dependency-version: 0.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 11:46:56 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
effacf7b91 chore(deps-dev): bump tailwindcss from 4.2.1 to 4.3.1 (#1142)
Bumps [tailwindcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/tailwindcss) from 4.2.1 to 4.3.1.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/tailwindcss)

---
updated-dependencies:
- dependency-name: tailwindcss
  dependency-version: 4.3.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-16 11:46:24 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5af1d042cf chore(deps): bump @radix-ui/react-toast from 1.2.15 to 1.2.16 (#1143)
Bumps [@radix-ui/react-toast](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toast) from 1.2.15 to 1.2.16.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toast/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toast)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-toast"
  dependency-version: 1.2.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 23:01:14 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
f848bea33f chore(deps): bump @radix-ui/react-slider from 1.3.6 to 1.4.0 (#1141)
Bumps [@radix-ui/react-slider](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/slider) from 1.3.6 to 1.4.0.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/slider/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/slider)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-slider"
  dependency-version: 1.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 22:59:51 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
94282b634e chore(deps): bump @radix-ui/react-tooltip from 1.2.8 to 1.2.9 (#1139)
Bumps [@radix-ui/react-tooltip](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/tooltip) from 1.2.8 to 1.2.9.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/tooltip/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/tooltip)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-tooltip"
  dependency-version: 1.2.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 22:55:48 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7b8298e292 chore(deps-dev): bump @types/chrome from 0.1.37 to 0.1.43 (#1138)
Bumps [@types/chrome](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/chrome) from 0.1.37 to 0.1.43.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/chrome)

---
updated-dependencies:
- dependency-name: "@types/chrome"
  dependency-version: 0.1.43
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 22:55:20 -04:00
AustinandGitHub 0e04d37ad1 fix silly pnpm yaml keyerror (#1140)
* fix silly pnpm yaml keyerror

* Another one
2026-06-15 22:49:12 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a5e86f8823 chore(deps): bump @radix-ui/react-visually-hidden from 1.2.4 to 1.2.5 (#1136)
Bumps [@radix-ui/react-visually-hidden](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/visually-hidden) from 1.2.4 to 1.2.5.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/visually-hidden/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/visually-hidden)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-visually-hidden"
  dependency-version: 1.2.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 22:16:55 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
5946833e4d chore(deps): bump maplibre-gl from 5.8.0 to 5.24.0 (#1132)
Bumps [maplibre-gl](https://github.com/maplibre/maplibre-gl-js) from 5.8.0 to 5.24.0.
- [Release notes](https://github.com/maplibre/maplibre-gl-js/releases)
- [Changelog](https://github.com/maplibre/maplibre-gl-js/blob/main/CHANGELOG.md)
- [Commits](https://github.com/maplibre/maplibre-gl-js/compare/v5.8.0...v5.24.0)

---
updated-dependencies:
- dependency-name: maplibre-gl
  dependency-version: 5.24.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 22:16:16 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
598ff8d08e chore(deps): bump zustand from 5.0.8 to 5.0.14 (#1131)
Bumps [zustand](https://github.com/pmndrs/zustand) from 5.0.8 to 5.0.14.
- [Release notes](https://github.com/pmndrs/zustand/releases)
- [Commits](https://github.com/pmndrs/zustand/compare/v5.0.8...v5.0.14)

---
updated-dependencies:
- dependency-name: zustand
  dependency-version: 5.0.14
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 22:15:54 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c710fea5fd chore(deps-dev): bump lint-staged from 16.3.3 to 17.0.7 (#1135)
Bumps [lint-staged](https://github.com/lint-staged/lint-staged) from 16.3.3 to 17.0.7.
- [Release notes](https://github.com/lint-staged/lint-staged/releases)
- [Changelog](https://github.com/lint-staged/lint-staged/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lint-staged/lint-staged/compare/v16.3.3...v17.0.7)

---
updated-dependencies:
- dependency-name: lint-staged
  dependency-version: 17.0.7
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 22:06:27 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
770acaa5e0 chore(deps): bump @radix-ui/react-toggle-group from 1.1.11 to 1.1.12 (#1134)
Bumps [@radix-ui/react-toggle-group](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/toggle-group) from 1.1.11 to 1.1.12.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/toggle-group/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/toggle-group)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-toggle-group"
  dependency-version: 1.1.12
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 22:03:37 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
293a963e0e chore(deps): bump @radix-ui/react-select from 2.2.6 to 2.3.0 (#1128)
Bumps [@radix-ui/react-select](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/select) from 2.2.6 to 2.3.0.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/select/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/select)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-select"
  dependency-version: 2.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 22:02:05 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
72ed413cf1 chore(deps): bump @radix-ui/react-checkbox from 1.3.3 to 1.3.4 (#1130)
Bumps [@radix-ui/react-checkbox](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/checkbox) from 1.3.3 to 1.3.4.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/checkbox/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/checkbox)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-checkbox"
  dependency-version: 1.3.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 21:58:29 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a290b3cd3d chore(deps-dev): bump autoprefixer from 10.4.27 to 10.5.0 (#1129)
Bumps [autoprefixer](https://github.com/postcss/autoprefixer) from 10.4.27 to 10.5.0.
- [Release notes](https://github.com/postcss/autoprefixer/releases)
- [Changelog](https://github.com/postcss/autoprefixer/blob/main/CHANGELOG.md)
- [Commits](https://github.com/postcss/autoprefixer/compare/10.4.27...10.5.0)

---
updated-dependencies:
- dependency-name: autoprefixer
  dependency-version: 10.5.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 21:57:23 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
dbe8953942 chore(deps-dev): bump @vitejs/plugin-react from 4.7.0 to 6.0.2 (#1127)
Bumps [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/tree/HEAD/packages/plugin-react) from 4.7.0 to 6.0.2.
- [Release notes](https://github.com/vitejs/vite-plugin-react/releases)
- [Changelog](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite-plugin-react/commits/plugin-react@6.0.2/packages/plugin-react)

---
updated-dependencies:
- dependency-name: "@vitejs/plugin-react"
  dependency-version: 6.0.2
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 21:37:33 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>Dan Ditomaso
3dec67f623 chore(deps-dev): bump typescript from 5.9.3 to 6.0.3 (#1113)
* chore(deps-dev): bump typescript from 5.9.3 to 6.0.3

Bumps [typescript](https://github.com/microsoft/TypeScript) from 5.9.3 to 6.0.3.
- [Release notes](https://github.com/microsoft/TypeScript/releases)
- [Commits](https://github.com/microsoft/TypeScript/compare/v5.9.3...v6.0.3)

---
updated-dependencies:
- dependency-name: typescript
  dependency-version: 6.0.3
  dependency-type: direct:development
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(tsconfig): align configs for typescript 6.0

Consolidate package tsconfigs to extend tsconfig.base.json and drop
options now redundant or invalid under TS 6 (strict, baseUrl,
allowImportingTsExtensions, strictNullChecks). Switch base module to
"preserve" and lib to ESNext. Fix apps/web typecheck script to invoke
tsc directly instead of recursive pnpm run tsc.

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Dan Ditomaso <dan.ditomaso@gmail.com>
2026-06-15 21:36:54 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
42de8e7403 chore(deps): bump vite from 7.3.5 to 8.0.16 (#1107)
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 7.3.5 to 8.0.16.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/main/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v8.0.16/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-version: 8.0.16
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 20:57:47 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8f0db66131 chore(deps): bump tailwind-merge from 2.6.1 to 3.6.0 (#1124)
Bumps [tailwind-merge](https://github.com/dcastil/tailwind-merge) from 2.6.1 to 3.6.0.
- [Release notes](https://github.com/dcastil/tailwind-merge/releases)
- [Commits](https://github.com/dcastil/tailwind-merge/compare/v2.6.1...v3.6.0)

---
updated-dependencies:
- dependency-name: tailwind-merge
  dependency-version: 3.6.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 20:56:04 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
bcfa1bd8cc chore(deps-dev): bump @tailwindcss/postcss from 4.2.1 to 4.3.1 (#1126)
Bumps [@tailwindcss/postcss](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-postcss) from 4.2.1 to 4.3.1.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/@tailwindcss-postcss)

---
updated-dependencies:
- dependency-name: "@tailwindcss/postcss"
  dependency-version: 4.3.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 20:25:33 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
cd3bfc3c51 chore(deps): bump @tailwindcss/vite from 4.2.1 to 4.3.1 (#1125)
Bumps [@tailwindcss/vite](https://github.com/tailwindlabs/tailwindcss/tree/HEAD/packages/@tailwindcss-vite) from 4.2.1 to 4.3.1.
- [Release notes](https://github.com/tailwindlabs/tailwindcss/releases)
- [Changelog](https://github.com/tailwindlabs/tailwindcss/blob/main/CHANGELOG.md)
- [Commits](https://github.com/tailwindlabs/tailwindcss/commits/v4.3.1/packages/@tailwindcss-vite)

---
updated-dependencies:
- dependency-name: "@tailwindcss/vite"
  dependency-version: 4.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 20:01:06 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
c4aa168af2 chore(deps): bump drizzle-orm from 0.36.4 to 0.45.2 (#1117)
Bumps [drizzle-orm](https://github.com/drizzle-team/drizzle-orm) from 0.36.4 to 0.45.2.
- [Release notes](https://github.com/drizzle-team/drizzle-orm/releases)
- [Commits](https://github.com/drizzle-team/drizzle-orm/compare/0.36.4...0.45.2)

---
updated-dependencies:
- dependency-name: drizzle-orm
  dependency-version: 0.45.2
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 19:56:49 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
afc28b6e16 chore(deps): bump @radix-ui/react-dropdown-menu from 2.1.16 to 2.1.17 (#1123)
Bumps [@radix-ui/react-dropdown-menu](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dropdown-menu) from 2.1.16 to 2.1.17.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dropdown-menu/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dropdown-menu)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-dropdown-menu"
  dependency-version: 2.1.17
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 19:46:46 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1590f17689 chore(deps): bump @radix-ui/react-scroll-area from 1.2.10 to 1.2.11 (#1122)
Bumps [@radix-ui/react-scroll-area](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/scroll-area) from 1.2.10 to 1.2.11.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/scroll-area/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/scroll-area)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-scroll-area"
  dependency-version: 1.2.11
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 19:44:00 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
e6cbd88a88 chore(deps): bump @radix-ui/react-switch from 1.2.6 to 1.3.0 (#1119)
Bumps [@radix-ui/react-switch](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/switch) from 1.2.6 to 1.3.0.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/switch/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/switch)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-switch"
  dependency-version: 1.3.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 19:39:03 -04:00
Ben MeadorsandGitHub cffa7f28ed fix(core): remove orphaned packages/core left by the #1050 squash-merge (#1120)
#1050 (Phase C) deleted packages/core in favor of @meshtastic/sdk, but the
squash-merge hit a delete/modify conflict with #1097 (which had modified
packages/core/package.json and src/meshDevice.ts), leaving those two files
behind as a broken stub. Its package.json still declared a tsdown build
(entry: "mod.ts") while mod.ts and the rest of the source were gone, so the
'Push to Main CI' build-and-package loop failed with:
  Error: Cannot find entry: "mod.ts"

Nothing depends on the workspace @meshtastic/core — the app resolves
@meshtastic/core to the JSR-published @jsr/meshtastic__core@2.6.6 — so finish
the deletion and drop the now-unused pnpm-lock importer.

Verified locally: frozen-lockfile install passes and the full build-and-package
loop builds every package green (sdk, sdk-react, sdk-storage-sqlocal, all
transports).
2026-06-15 17:44:58 -05:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
bb326798f9 chore(deps): bump @radix-ui/react-label from 2.1.8 to 2.1.9 (#1118)
Bumps [@radix-ui/react-label](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/label) from 2.1.8 to 2.1.9.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/label/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/label)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-label"
  dependency-version: 2.1.9
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 18:23:38 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
6719211cdb chore(deps): bump react-qrcode-logo from 4.0.0 to 4.1.0 (#1116)
Bumps [react-qrcode-logo](https://github.com/gcoro/react-qrcode-logo) from 4.0.0 to 4.1.0.
- [Changelog](https://github.com/gcoro/react-qrcode-logo/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gcoro/react-qrcode-logo/compare/v4.0.0...v4.1.0)

---
updated-dependencies:
- dependency-name: react-qrcode-logo
  dependency-version: 4.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 18:19:03 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
00251a5003 chore(deps): bump the react-ecosystem group across 1 directory with 4 updates (#1108)
Bumps the react-ecosystem group with 4 updates in the / directory: [react](https://github.com/facebook/react/tree/HEAD/packages/react), [@types/react](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react), [react-dom](https://github.com/facebook/react/tree/HEAD/packages/react-dom) and [@types/react-dom](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/react-dom).


Updates `react` from 19.2.4 to 19.2.7
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react)

Updates `@types/react` from 18.3.28 to 19.2.17
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react)

Updates `react-dom` from 19.2.4 to 19.2.7
- [Release notes](https://github.com/facebook/react/releases)
- [Changelog](https://github.com/react/react/blob/main/CHANGELOG.md)
- [Commits](https://github.com/facebook/react/commits/v19.2.7/packages/react-dom)

Updates `@types/react-dom` from 18.3.7 to 19.2.3
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

Updates `@types/react-dom` from 18.3.7 to 19.2.3
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/react-dom)

---
updated-dependencies:
- dependency-name: "@types/react"
  dependency-version: 19.2.17
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: react-ecosystem
- dependency-name: "@types/react-dom"
  dependency-version: 19.2.3
  dependency-type: direct:development
  update-type: version-update:semver-major
  dependency-group: react-ecosystem
- dependency-name: react
  dependency-version: 19.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: react-ecosystem
- dependency-name: react-dom
  dependency-version: 19.2.7
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: react-ecosystem
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 18:14:07 -04:00
dc9749c042 feat: scaffold @meshtastic/sdk + signals/sqlocal persistence migration (#1050)
* feat(sdk): scaffold @meshtastic/sdk + @meshtastic/sdk-react

Adds two new packages laying the foundation for a domain-driven migration
away from @meshtastic/core.

packages/sdk
- DDD feature slices: device, chat, nodes, channels, config, telemetry,
  position, traceroute, files. Each with domain/application/infrastructure/state.
- Shared kernel under core/: MeshClient orchestrator, Transport interface
  (byte-compat with existing transport-* packages), EventBus (typed pub/sub),
  packet codec, Queue, Xmodem, signals helpers, tslog factory.
- Signals via @preact/signals-core. Application use-cases return
  Result<T,E> via better-result; legacy ports keep throwing.
- shim/ re-exports the legacy MeshDevice/Types/Utils API so
  packages/web continues to build unchanged.
- createFakeTransport() under @meshtastic/sdk/testing.
- 16 vitest tests incl. end-to-end fake-transport integration.

packages/sdk-react
- MeshProvider + useSignal/useSignalValue/useClient adapters.
- Hooks: useDevice, useConnection, useChat, useNodes, useNode,
  useChannels, useChannel, useConfig, useModuleConfig, useTelemetry,
  usePosition, useTraceroute, useFileTransfer, useFavoriteNode, useIgnoreNode.
- jsdom-backed hook tests.

Root README rewritten with packages table, architecture, and workflow.

* refactor(web): import @meshtastic/sdk instead of @meshtastic/core

Mechanical swap across all 74 @meshtastic/core imports in packages/web/src.
The SDK's shim layer re-exports the legacy MeshDevice/Types/Utils/Protobuf
surface, so no source edits are required beyond the import path. All 294
web vitest tests still pass.

This is Phase B step 1: web now runs on @meshtastic/sdk's MeshDevice shim.
Per-slice store migrations (messageStore/nodeDBStore/deviceStore →
useChat/useNodes/useDevice etc.) land in follow-up commits.

* feat(sdk): add MeshRegistry + multi-client React providers

Phase B prep for web migration. Web holds multiple simultaneous device
connections keyed by ConnectionId, so per-slice hook migrations need a
registry-aware provider.

packages/sdk
- MeshRegistry: Map<ConnectionId, MeshClient> with signals for list/active/activeId.
  create()/get()/has()/remove()/setActive().
- First-created client auto-activates.
- remove() disconnects the client and rotates active to another entry.
- 4 vitest cases covering create, auto-activate, duplicate rejection, and remove.

packages/sdk-react
- MeshRegistryProvider + MeshRegistryContext.
- useMeshRegistry, useOptionalMeshRegistry, useActiveClient, useClientById(id).
- useClient now falls back to the registry's active client when no direct
  <MeshProvider> is present, so existing hooks work unchanged under a
  registry-backed app.

No web-facing changes in this commit; used by follow-up slice migrations.

* refactor(sdk-react): rename useDevice → useMeshDevice

Prevents collision with packages/web's own useDevice() Zustand hook. All
internal exports + tests updated; no behavior change.

Callers migrating off @meshtastic/core should use useMeshDevice() from
@meshtastic/sdk-react going forward.

* feat(web): mount MeshRegistryProvider at app root

Introduces a single app-wide MeshRegistry in packages/web/src/core/meshRegistry.ts
and wraps the RouterProvider in <MeshRegistryProvider>. Registry starts empty;
useConnections continues to instantiate the legacy MeshDevice shim. Subsequent
slice migrations will swap useConnections over to registry.create() and move
consumers onto useMeshDevice()/useChat()/etc from @meshtastic/sdk-react.

Adds @meshtastic/sdk-react as a web dependency. No behavior change; web tests
(294) and production build still pass.

* feat(web,sdk): register per-connection MeshClient in registry

packages/sdk
- MeshRegistry.register(id, client): adopts an externally-constructed
  MeshClient. Complements create() for migration paths where a legacy shim
  already owns the client.
- MeshRegistry.unregister(id): drops the mapping without disconnecting, for
  cases where the caller has torn down the transport itself.
- Legacy MeshDevice shim exposes its inner MeshClient as `meshClient` so
  consumers can adopt it into the registry.

packages/web
- useConnections.setupMeshDevice now registers the shim's MeshClient with the
  app-wide meshRegistry and marks it active on connect.
- removeConnection unregisters from the registry.
- Legacy Zustand deviceStore wiring is unchanged; follow-up commits will move
  read paths to useMeshDevice/useChat etc. and remove the duplicated fields.

No behavior change visible to users. Web tests (294) + SDK tests (20) still pass.

* feat(sdk): add MessageRepository port + InMemoryMessageRepository

Lays the persistence groundwork for the chat slice migration.

- MessageRepository port defines paginated reads (loadRecent, loadBefore),
  atomic writes (append, appendBatch), state updates, and retention pruning.
  Conversation keyed by ConversationKey tagged union (channel | direct peer).
- RetentionPolicy: maxPerBucket + olderThanMs knobs. Consumer decides.
- InMemoryMessageRepository ships with SDK as default + test fixture.
- ChatClient accepts { repository?, retention?, initialLoadLimit? }.
  Lazy-hydrates per conversation on first subscribe; writes through on every
  inbound message; prunes after append when retention policy is set.
- ChatClient.loadOlder(conv, before, limit) for pagination UI.
- ChatStore.prepend() for older-first inserts.

Tests: 5 new cases for InMemoryMessageRepository (paginate, update state,
retention). 25 SDK tests total, all green. Web build unchanged.

Paves the way for @meshtastic/sdk-storage-sqlocal to implement this port
against SQLite/OPFS in a follow-up.

* feat(sdk-storage-sqlocal): SQLite WASM persistence adapters

New workspace package implementing @meshtastic/sdk repository ports against
sqlocal (SQLite WASM + OPFS) with Drizzle-typed queries.

Schema (single DB, multi-device aware via device_id column)
- messages: chat history. Indexes on (device_id, conversation_key, rx_time)
  for fast pagination and on (device_id, state) for pending lookups.
- nodes: NodeDB snapshot per device (stub schema, repo lands with PR #7).
- telemetry: per-node ring buffer of readings (stub).
- _schema: migration version table.
- Hand-written DDL migrations in src/schema/migrations.ts; applied at boot.

createSqlocalDb({ databasePath })
- Opens OPFS DB, applies migrations, returns Drizzle client typed against the
  schema.
- Single instance per origin; sqlocal serializes writes via OPFS file locks.

SqlocalMessageRepository
- Implements MessageRepository: paginated loadRecent / loadBefore, append /
  appendBatch with onConflictDoNothing, updateState, prune (maxPerBucket via
  windowed DELETE + olderThanMs).
- Optional MultiTabCoordinator broadcasts messages-changed events on append.

MultiTabCoordinator
- BroadcastChannel pub/sub for cross-tab change notifications (no-ops if API
  unavailable, e.g. Node).
- acquireLock() wraps navigator.locks.request with fall-through for non-browser
  contexts.

Testing
- src/testing/createMemoryDb.ts: in-memory sql.js + Drizzle, same surface as
  the sqlocal connection. Lets repository tests run on Node CI.
- 6 SqlocalMessageRepository tests (pagination, retention, multi-device
  isolation) + 2 MultiTabCoordinator tests pass on sql.js.

Notes
- @meshtastic/sdk pkg.json now points types at ./mod.ts so workspace consumers
  resolve types directly from source. Production publish path needs a separate
  follow-up to emit a stable mod.d.ts.
- Storage pkg ships ESM only; dts disabled until tsdown's hashed-name emit
  is reconciled with mod.d.ts resolution. Workspace consumption already gets
  full types from source.

* feat(web): persist chat history via @meshtastic/sdk-storage-sqlocal

Plumbs the SDK chat slice through the OPFS-backed SQLite repository so
inbound and outbound text messages survive page reloads, with retention
capped at 1000 messages per conversation or 90 days, whichever hits first.

- packages/sdk legacy MeshDevice shim now accepts MeshClientOptions
  (chat, configId, logger). Backwards-compatible with the old
  `new MeshDevice(transport, configIdNumber)` form.
- packages/web/src/core/sdkStorage.ts: lazy singletons for the shared
  SqlocalDb and the cross-tab MultiTabCoordinator. The DB opens on first
  call so test runs that never connect stay headless-safe.
- useConnections.setupMeshDevice is now async, awaits getStorageDb, and
  passes a SqlocalMessageRepository scoped to the connection id. Falls
  back to the SDK's InMemoryMessageRepository if sqlocal init fails (no
  OPFS support, etc.).
- Vite worker format set to "es" because sqlocal's worker is ES-module
  and rolldown rejects iife with code-splitting.
- COOP/COEP dev headers were already in vite.config.ts; no further
  changes required for OPFS.

Web tests (294) and production build still green.

This is the runtime payoff of PR #5: a fresh page load populates chat from
SQLite via lazy pagination instead of rehydrating 1000 messages into memory.
The legacy Zustand messageStore is still in place for now; PR #6 (chat
slice migration) will retire it and switch UI components to useChat.

* test: testing strategy + 19 new SDK / storage / hook tests

Adds TESTING.md documenting six tiers (unit / slice / client / storage /
hook / E2E), per-package coverage gates, and the audit of where we
currently sit.

Slice tests in @meshtastic/sdk
- NodeMapper proto round-trip; NodesClient list signal updates.
- ChannelsClient indexes by channel number.
- ConfigClient merges Config + ModuleConfig variants.
- TelemetryClient latest + history per node.
- PositionClient byNode + list.
- ChatClient persistence: hydrate on first subscribe, paginate via
  loadOlder, persist inbound messages through the repository. Fixes a
  reverse-iteration bug in loadOlder discovered by the new test.

Hook tests in @meshtastic/sdk-react
- New tests/hooks.registry.test.tsx covers useMeshDevice, useNodes,
  useNode, useChannels under <MeshRegistryProvider>, plus an active-
  client switch round-trip.

Storage tests in @meshtastic/sdk-storage-sqlocal
- migrations.test.ts validates v1 DDL creates messages/nodes/telemetry/
  _schema and indexes; CREATE IF NOT EXISTS is idempotent.
- MultiTabCoordinator.broadcast.test.ts proves cross-tab BroadcastChannel
  delivery between two coordinators in the same process.
- New vitest.browser.config.ts + tests/sqlocal-opfs.browser.test.ts run
  under @vitest/browser (Playwright provider) for real OPFS round-trip
  verification. Wired as `pnpm test:browser`. Browser test files end in
  `.browser.test.ts` and are excluded from the Node runner.

E2E / firmware-simulator tier (TESTING.md §"E2E / simulator") is scoped
for a follow-up — needs CI Docker for meshtasticd.

Totals after this change:
- @meshtastic/sdk: 36 tests (was 25)
- @meshtastic/sdk-react: 8 tests (was 2)
- @meshtastic/sdk-storage-sqlocal: 12 tests (was 8)
- meshtastic-web: 294 tests (unchanged)

* feat(web): MessagesPage reads chat history from SDK / SQLite

Adds an adapter hook that bridges sdk-react's `useChat` and `useDirectChat`
to the legacy `Message` shape MessagesPage / ChannelChat / MessageItem
expect. The page now renders messages directly from the OPFS-backed
SqlocalMessageRepository — page reload hydrates lazily (last 50 per
conversation) instead of pulling 1000+ rows from IndexedDB into memory.

packages/sdk-react
- useChat() gains loadOlder(before, limit) for paginated backfill.
- New useDirectChat(peer) hook covering DM conversations.

packages/web
- src/core/hooks/useChatLegacy.ts: maps SDK `Message` → legacy
  `messageStore/types.ts` Message shape, including state translation
  (SDK Pending → legacy Waiting).
- MessagesPage flips broadcast and direct chat reads to useChatLegacy.
  getMessages call sites removed; setMessageState retained on the legacy
  store for outbound bookkeeping until the next migration step.

Drafts, unread counts, activeChat / chatType continue to live in the
Zustand messageStore — they are UI-only state and stay where they are
per the locked architecture decision. The legacy store's saveMessage,
getMessages, setMessageState, and persistence path remain in place for
now; PR cleanup follow-up will retire them once outbound state and
"delete all messages" flows are switched to the SDK.

Web test suite: 294 still green. Production Vite build clean.

* feat(web): MessagesPage outbound send through SDK ChatClient

Switches the send-text path from `connection?.sendText(...)` (legacy
MeshDevice) to `client.chat.send({ text, destination, channel })` on the
active MeshClient pulled from MeshRegistry via useActiveClient().

Side effects:
- Outbound message state (Ack / Failed) is now driven entirely by the
  SDK chat slice via routing-packet subscriptions; the manual
  setMessageState calls are removed.
- The legacy Zustand `useMessages().setMessageState` and `MessageState`
  imports are no longer used by MessagesPage. Drafts and unread counts
  still live in the Zustand store.
- `getMyNode` is no longer needed in this page (was only used to label
  the now-removed direct-message state updates).

Web tests (294) still green; production Vite build clean.

* feat(sdk,web): drafts persisted in SDK chat slice (SQLite-backed)

Moves per-conversation draft text out of the legacy Zustand messageStore
into the SDK chat slice so drafts share the same persistence + signal
machinery as messages. MessageInput now binds directly to the SDK; the
legacy `useMessages().getDraft/setDraft/clearDraft` API is no longer
called from the input.

packages/sdk
- New DraftRepository port + InMemoryDraftRepository default.
- ChatClient.drafts namespace: get(key) returns a ReadonlySignal<string>;
  set/clear keyed by ConversationKey.
- ChatClient.send auto-clears the draft for the resolved conversation on
  success (parity with prior Zustand clearDraft-on-send behavior).
- Lazy hydrate from the DraftRepository on first read of a conversation.
- 3 new tests in ChatClient.drafts.test.ts.

packages/sdk-react
- New useDraft(conversation) hook returning { text, setText, clear }.

packages/sdk-storage-sqlocal
- New `drafts` table (device_id + conversation_key composite PK, text,
  updated_at). Drizzle schema in src/schema/drafts.ts.
- Migration v2 in src/schema/migrations.ts creates the table on first
  open of an existing v1 DB.
- SqlocalDraftRepository implementing DraftRepository: load/save/clear/
  loadAll, scoped by device_id, upsert on conflict, delete on empty save.
- 6 new tests covering save/load round-trip, empty-string deletion,
  upsert, multi-device isolation, loadAll.

packages/web
- useConnections wires the SqlocalDraftRepository alongside the existing
  SqlocalMessageRepository per registered MeshClient.
- MessageInput accepts `conversation: ConversationKey` instead of the
  prior `to: Types.Destination` — fixes the legacy bug where every
  broadcast channel shared a single draft slot.
- MessagesPage passes the appropriate ConversationKey for direct/
  broadcast chats.
- MessageInput.test.tsx rewritten to mock useDraft from sdk-react.

Test counts: sdk 39 (+3), sdk-storage-sqlocal 18 (+6), web 294 unchanged.
Production Vite build clean.

* chore(web): drop saveMessage path from subscriptions, delete dead DTO

The SDK chat slice now persists every inbound/outbound text packet via the
SqlocalMessageRepository wired in useConnections, so the legacy Zustand
saveMessage path in subscriptions.ts was writing to a store no UI code
reads from.

- subscriptions.ts: removed saveMessage call + PacketToMessageDTO usage.
  Unread-count increments retained as-is (cross-cutting concern, migrates
  in a separate commit).
- subscribeAll's messageStore parameter retained as `_messageStore` for
  callsite stability while the rest of the legacy store is being retired.
- Deleted packages/web/src/core/dto/PacketToMessageDTO.ts (no remaining
  consumers; SDK has its own MessageMapper at
  packages/sdk/src/features/chat/infrastructure/MessageMapper.ts).

Web tests (294) still green; production build clean.

Out of scope, queued:
- useConnections refactor (the hook is overdue for cleanup)
- Strip dead methods from the Zustand messageStore (saveMessage,
  getMessages, setMessageState, getDraft, setDraft, clearDraft +
  Zustand-persist + IDB wrapper). Requires a follow-up sweep of the
  remaining test files that mock those methods.
- Migrate unread counts to the SDK (cross-cutting between chat + nodes).

* feat(sdk,web): ChatClient.clearConversation + clearAll; wire DeleteMessagesDialog

Rounds out the chat slice with destructive operations so the UI dialog no
longer needs the legacy Zustand deleteAllMessages.

packages/sdk
- MessageRepository port gains clearConversation(key); InMemory and
  Sqlocal adapters implement it (SQL: scoped DELETE by conversation).
- ChatStore gains clearBucket(key) + clearAll().
- ChatClient.clearConversation(conv) and ChatClient.clearAll(): empty the
  in-memory store, drop the hydrated-marker so a future subscribe
  re-fetches fresh, then delete from the repository. Repository failures
  are swallowed — UI must not get stuck behind a write error.

packages/sdk-storage-sqlocal
- SqlocalMessageRepository.clearConversation: DELETE FROM messages WHERE
  (device_id, conversation_key) match.

packages/web
- DeleteMessagesDialog swaps useMessages().deleteAllMessages for
  useActiveClient()?.chat.clearAll(). No-active-client path is a no-op
  but still closes the dialog.
- Test file updated: mocks useActiveClient; new case covers
  no-active-client safety.

Totals: sdk 39 tests unchanged (clearConversation tested transitively via
DeleteMessagesDialog; adapter-specific test queued for follow-up),
sdk-storage-sqlocal 18 unchanged, web 295 (+1).

* feat(sdk,sdk-storage-sqlocal,web): NodesRepository port + SQLite persistence

Adds SDK-side node persistence so a fresh page load rehydrates the mesh
NodeDB from disk before any device packets arrive. Web's existing Zustand
nodeDBStore continues to work unchanged in parallel — UI consumer
migration to useNodes/useNode lands in follow-up commits.

packages/sdk
- New NodesRepository port: loadAll / get / upsert / upsertBatch / remove /
  clear. InMemoryNodesRepository ships as the default.
- NodesClient takes optional { repository }, hydrates on construction,
  writes through on every onNodeInfoPacket, and keeps live signal +
  persistence in lockstep. remove/reset clear the repository alongside the
  store + drive the legacy admin message.
- MeshClientOptions exposes a `nodes` slot mirroring the existing `chat`
  slot.

packages/sdk-storage-sqlocal
- New SqlocalNodesRepository implementing the port. user / position /
  deviceMetrics serialized as base64-encoded protobuf bytes (stable
  across schema additions). Subpath export at "@meshtastic/sdk-storage-
  sqlocal/nodes".
- 6 vitest cases covering upsert + loadAll round-trip, overwrite, proto
  round-trip preserves user fields, remove, clear, multi-device isolation.

packages/web
- useConnections opens a SqlocalNodesRepository alongside the chat /
  draft repos and passes it to the new MeshDevice constructor.

Test counts: sdk 39 (unchanged — repo round-trip exercised in storage
adapter tests), sdk-storage-sqlocal 24 (+6), web 295 unchanged. Build
clean.

* feat(sdk,web): SDK Node carries hops/mqtt/key-verified; legacy adapter hook

Lays the groundwork for migrating web's 31 nodeDB consumers off the
Zustand `useNodeDB().getNodes/getNode` API onto SDK signals, without a
big-bang rewrite.

packages/sdk
- Node domain entity gains channel, viaMqtt, hopsAway, isKeyManuallyVerified
  fields. NodeMapper.fromProto now copies these from the inbound
  Protobuf.Mesh.NodeInfo. Required so downstream UIs that show "X hops
  away" / "via MQTT" / "encryption verified" can read SDK nodes without
  losing fidelity.

packages/web
- New core/hooks/useNodesLegacy.ts: useNodesLegacy() returns
  Protobuf.Mesh.NodeInfo[] derived from SDK signals; useNodeLegacy(num)
  returns a single NodeInfo. Components migrate one at a time by swapping
  useNodeDB().getNodes / getNode call sites for these hooks; templates
  stay unchanged because the result shape matches.

No consumer rewrites in this commit — that is per-component follow-up
work to keep diffs reviewable. Web tests (295) still green; production
build clean. SDK 39 / sdk-storage-sqlocal 24 unchanged.

* refactor(web): NodesPage reads node list from SDK signals

First consumer migration off the legacy Zustand nodeDBStore onto
SDK-managed nodes. Pages/Nodes/index.tsx now pulls the full node array
through useNodesLegacy() — which subscribes to the SDK NodesClient
signal underneath — and applies the existing nodeFilter predicate
client-side via useMemo.

Behavior parity:
- Same Protobuf.Mesh.NodeInfo shape rendered in the table (the legacy
  adapter ensures channel / viaMqtt / hopsAway / publicKey survive).
- Same debounce semantics — only the underlying source changed.
- hasNodeError + nodeErrors continue to come from the Zustand
  nodeDBStore until PKI-error tracking is migrated to the SDK in a
  follow-up commit (validation logic still in
  packages/web/src/core/stores/nodeDBStore/nodeValidation.ts).

The legacy nodeDBStore still populates from subscriptions.ts, so this
rewrite is reversible and runs alongside the SDK source. Web tests
(295) still green; production Vite build clean.

* refactor(web): migrate 9 nodeDB consumers onto SDK adapter

Sweeps the most-touched UI paths off useNodeDB().getNode/getMyNode/getNodes
and onto the useNodesLegacy / useNodeLegacy / useMyNodeLegacy adapters
introduced earlier. Behaviour parity preserved — the adapter returns the
same Protobuf.Mesh.NodeInfo shape components already render.

Migrated:
- components/Sidebar.tsx — myNode + node count from SDK signals.
- components/CommandPalette/index.tsx — node lookup for connection labels.
- components/UI/Avatar.tsx — single-node lookup.
- components/Dialog/RemoveNodeDialog.tsx — selected node display.
- components/Dialog/PKIBackupDialog.tsx — myNode for download/print headers.
- components/Dialog/LocationResponseDialog.tsx — sender lookup.
- components/Dialog/TracerouteResponseDialog.tsx — endpoint lookups.
- components/Dialog/NodeDetailsDialog.tsx — selected node detail.
- components/PageComponents/Settings/User.tsx — myNode for owner edit.
- components/PageComponents/Settings/Position.tsx — myNode for current position.
- components/PageComponents/Messages/TraceRoute.tsx — hop name lookup.
- components/PageComponents/Messages/MessageItem.tsx — myNode (suspending) +
  message author. The polling Suspense fallback now retriggers on the SDK
  signal instead of polling the Zustand store directly.
- components/PageComponents/Map/Popups/WaypointDetail.tsx — locked-to node.
- pages/Messages.tsx — sidebar node list + selected peer lookup.
- pages/Map/index.tsx — full filtered list + myNode for fitting. Drops the
  now-dead NODEDB_DEBOUNCE_MS constant since the SDK signal layer handles
  re-render coalescing internally.
- TraceRoute.test.tsx mocks updated to mock useNodesLegacy instead of
  useNodeDB.

NodesLayer.tsx, RemoveNodeDialog (removeNode), ResetNodeDbDialog, and
RefreshKeysDialog still depend on hasNodeError / removeNode /
removeAllNodes on the legacy nodeDB store — those move when PKI-error
tracking and the admin-message paths are migrated to the SDK in the
unread/cleanup follow-up.

Web tests: 295 still green; production Vite build clean. No SDK changes
in this commit.

* refactor(web): rename node legacy adapters to *AsProto

The "Legacy" suffix on the node-adapter hooks reads as if the hook
itself is deprecated, when in fact the hook is the bridge: it converts
SDK Node domain entities into Protobuf.Mesh.NodeInfo so consumer
templates that destructure proto fields keep working during the
migration.

- useNodesLegacy → useNodesAsProto
- useNodeLegacy → useNodeAsProto
- useMyNodeLegacy → useMyNodeAsProto
- File renamed to packages/web/src/core/hooks/useNodesAsProto.ts.

All 16 call sites updated. Test mock in
packages/web/src/components/PageComponents/Messages/TraceRoute.test.tsx
updated to the new module path.

Behaviour unchanged. 295 tests still green; production Vite build clean.

(useChatLegacy follows the same pattern but maps to a hand-written web
type, not a proto — it stays as-is for now and is queued for renaming
once the broader chat-store cleanup lands.)

* refactor(web): useFavoriteNode + useIgnoreNode drive SDK NodesClient

The favorite/ignore admin-message paths now go through the SDK NodesClient
instead of the legacy Zustand sendAdminMessage + manual proto build.

- useFavoriteNode: meshClient.nodes.favorite(nodeNum) /
  meshClient.nodes.unfavorite(nodeNum). The SDK already has
  FavoriteNodeUseCase + AdminMessageSender behind these APIs.
- useIgnoreNode: meshClient.nodes.ignore / .unignore.
- Both still mirror the optimistic flag flip into the legacy nodeDB store
  via updateFavorite / updateIgnore until that store is fully retired —
  same TODO as before, "wait for ack before flipping".

Tests rewritten to mock @meshtastic/sdk-react via vi.hoisted (so the
factory captures the spy without hitting the vi.mock hoist trap):
- assert SDK favorite/unfavorite/ignore/unignore calls
- assert legacy store mirror still fires
- assert toast + longName fallback behaviour preserved
- assert no-op when the node isn't in the SDK store yet (parity with
  the prior "getNode returned undefined" guard)

Web tests: 295 still green; production Vite build clean.

* refactor(web): ResetNodeDb + RefreshKeys dialogs read SDK; safer adapter

- ResetNodeDbDialog: connection.resetNodes() → meshClient.nodes.reset();
  on success it now also calls meshClient.chat.clearAll() instead of the
  legacy useMessages().deleteAllMessages(). PKI-error tracking and the
  in-memory nodeDB still get cleared via removeAllNodeErrors /
  removeAllNodes since those subsystems have not yet migrated. Test
  rewritten to mock useActiveClient via vi.hoisted.
- RefreshKeysDialog: drops useNodeDB().getNode in favour of
  useNodeAsProto for the missing-key node display. Test wraps render in
  a MeshRegistryProvider with an empty registry so the adapter resolves
  cleanly with no active client.

Adapter hardening (useNodesAsProto.ts)
- Switched off useNodes() / useMeshDevice() (which both throw outside a
  MeshProvider/MeshRegistryProvider with an active client) onto
  useActiveClient() + a no-op signal fallback. The hooks now return [] /
  undefined when there is no active client instead of throwing, which
  fixes RefreshKeysDialog's "no error → render null" path under tests
  that don't connect a device.

Web tests: 295 still green; production Vite build clean.

* feat(sdk,sdk-react,web): node PKI / routing-error tracking on the SDK

Moves PKI mismatch / duplicate-key validation and routing-error tracking
out of the legacy Zustand nodeDBStore and into the SDK NodesClient so
every consumer reads the same source of truth. Validates Android's gap
analysis: Meshtastic-Android does not track per-node PKI errors at all,
so the SDK must own this concern client-side.

packages/sdk
- New NodeError + NodeErrorType (Routing_Error | "MISMATCH_PKI" |
  "DUPLICATE_PKI"). Mirrors the previous web-only types.
- New nodeValidation infrastructure mapper (pure): byte-equal compare on
  publicKey, returns ValidatedNodeInfo { accepted?, error? }. Ports the
  detection rules verbatim from
  packages/web/src/core/stores/nodeDBStore/nodeValidation.ts.
- NodesClient gains an errors signal + errorFor / hasError / setError /
  clearError / clearAllErrors API. handleIncoming runs validation before
  upserting; conflicts skip the store write and record an error. A clean
  refresh of a previously-flagged node clears the error.
- handleRoutingPacket records PKI_UNKNOWN_PUBKEY / PKI_FAILED /
  PKI_SEND_FAIL_PUBLIC_KEY / NO_CHANNEL against packet.from.
- 5 new vitest cases covering MISMATCH_PKI, DUPLICATE_PKI, error-clear-on-
  refresh, routing-error capture, clearError / clearAllErrors. SDK total:
  44 tests, all green.

packages/sdk-react
- New useNodeErrors / useNodeError(num) / useHasNodeError(num) hooks.
  All resolve through useActiveClient() and fall back to empty when no
  client is present.

packages/web
- pages/Nodes/index.tsx, pages/Messages.tsx, components/PageComponents/
  Map/Layers/NodesLayer.tsx swap useNodeDB().hasNodeError for the SDK
  useNodeErrors hook + a memoised Set lookup.
- components/Dialog/RefreshKeysDialog/RefreshKeysDialog.tsx reads the
  current error from useNodeError(activeChat); useRefreshKeysDialog.ts
  drives meshClient.nodes.clearError + meshClient.nodes.remove instead
  of the legacy store. The companion test still passes through the
  empty-MeshRegistry render path because useNodeError tolerates a
  missing active client.
- core/subscriptions.ts no longer calls nodeDB.setNodeError on PKI /
  no-channel routing packets — the SDK records those itself. The dialog
  open trigger stays here.
- pages/Nodes/index.tsx drops the now-dead NODEDB_DEBOUNCE_MS constant.

Web tests: 295 still green; production Vite build clean. The legacy
nodeDB still runs its internal validation but its error map is now a
dead end — removal queued in the plan-file follow-up alongside the rest
of the nodeDB cleanup.

* chore(web): remove dead PKI / nodeError paths from legacy nodeDB

Now that the SDK NodesClient owns public-key validation and per-node
error tracking, the legacy Zustand mirrors are dead code.

packages/web/src/core/stores/nodeDBStore/index.ts
- Drops the nodeErrors map + setNodeError / getNodeError / hasNodeError /
  clearNodeError / removeAllNodeErrors methods from the NodeDB interface
  and factory.
- Drops the validateIncomingNode call inside addNode and inside
  setNodeNum's merge path; legacy mirror is now a straight last-write-
  wins shallow merge. The SDK runs validation independently against its
  own snapshot.
- Drops the nodeErrors entries from the persisted partialize shape.

packages/web/src/core/stores/nodeDBStore/types.ts
- Removes NodeError + NodeErrorType. ProcessPacketParams stays.

packages/web/src/core/stores/nodeDBStore/nodeValidation.ts — deleted
(SDK ports it at packages/sdk/src/features/nodes/infrastructure/
nodeValidation.ts and exposes the verdict via NodesClient).

packages/web/src/core/stores/index.ts — drop the dead NodeErrorType
re-export.

packages/web/src/core/stores/nodeDBStore/nodeDBStore.test.tsx
- Removes tests covering the migrated PKI behaviour (errors map,
  MISMATCH, DUPLICATE, "unions nodeErrors") — equivalent coverage now
  lives in packages/sdk/src/features/nodes/NodesClient.errors.test.ts.
- Trims the surviving merge-semantics tests to the simpler last-write-
  wins shape.
- The "selector re-renders" test swaps the deleted setNodeError mutation
  for an updateFavorite call to keep the slice-stability assertion alive.
- The "addNodeDB instance identity" assertion relaxes from .toBe to
  content equality — immer's pruneStaleNodes path can reseat the entry,
  but the registered DB's id stays stable.

components/Dialog/ResetNodeDbDialog/ResetNodeDbDialog.tsx — calls
meshClient.nodes.clearAllErrors() instead of the legacy
removeAllNodeErrors. Test mocks updated.

components/Dialog/RefreshKeysDialog/* — already migrated to SDK errors
in the previous commit; no further changes here.

Test counts: web 290 (was 295 — 5 PKI-tracking tests retired in
favour of 5 equivalent SDK tests). Production Vite build clean.

* feat(sdk,web): SDK NodesClient owns user/position/lastHeard/snr +
                 favourite-flag flips; legacy mirror retired

The SDK NodesClient now subscribes to onUserPacket / onPositionPacket /
onMeshPacket so user-record updates, GPS updates, and lastHeard / snr
refreshes flow into the signal-backed store + repository directly. The
favourite / ignored toggles also flip the local flag once the admin
message resolves successfully, so the UI no longer needs to mirror the
state into a parallel Zustand store.

packages/sdk
- NodesClient: new private patch(num, partial) helper that shallow-
  merges into the existing entry (or seeds a placeholder Node) and
  upserts to the repository in one shot. Used by:
  - onUserPacket → patch user
  - onPositionPacket → patch position
  - onMeshPacket → patch lastHeard / snr (replaces the legacy nodeDB
    processPacket flow)
  - favorite / unfavorite / ignore / unignore — flag flip on Result.ok
- NodesClient.reset({ keepMyNode? }) — preserves the local node entry
  when requested. Mirrors the previous removeAllNodes(true) semantics
  the ResetNodeDb dialog relied on.

packages/web
- core/subscriptions.ts: drops the onUserPacket / onPositionPacket /
  onNodeInfoPacket / onMeshPacket → legacy nodeDB write paths. The
  unused nodeDB parameter is renamed `_nodeDB` for callsite stability;
  it will disappear when the store itself is deleted in a follow-up.
- core/hooks/useFavoriteNode + useIgnoreNode: drop the legacy
  updateFavorite / updateIgnore mirror — the SDK flips the flag on
  success.
- components/Dialog/RemoveNodeDialog: now calls meshClient.nodes.remove
  exclusively; no legacy fall-through.
- components/Dialog/ResetNodeDbDialog: calls
  meshClient.nodes.reset({ keepMyNode: true }) and meshClient.chat.clearAll().
  No legacy nodeDB calls.
- Test mocks for useFavoriteNode / useIgnoreNode / ResetNodeDbDialog
  pruned to match.

Test counts: sdk 44 (unchanged), sdk-react 8, sdk-storage-sqlocal 24,
web 290. Production Vite build clean.

Remaining surface on the legacy nodeDB: addNode / addUser / addPosition /
processPacket / setNodeError-equivalent are now unused; the store retains
only updateFavorite / updateIgnore (no callers) and the per-device
plumbing required by the deviceContext hooks. Full deletion of
nodeDBStore queued in the plan-file follow-up.

* chore(web): drop leftover useNodeDB from Position settings page

useMyNodeAsProto already replaces getMyNode here; the import was left
behind during the nodeDB mirror retirement.

* chore(web): delete legacy nodeDBStore — SDK NodesClient is the only source of truth

The Zustand nodeDBStore had no remaining writers (the mirror in
subscriptions.ts was retired in 005d0000) and no remaining readers
outside of bookkeeping (addNodeDB / setNodeNum / removeNodeDB calls
that fed nothing). Drop the whole store along with its persistence
shim and the persistNodeDB feature flag.

- Delete packages/web/src/core/stores/nodeDBStore/.
- Drop the _nodeDB parameter from subscribeAll; it was unused.
- Drop addNodeDB / useNodeDBStore wiring from useConnections; the
  meshDeviceId reuse check now keys off the device store instead.
- useNewNodeNum no longer pokes the nodeDB.
- FactoryResetDeviceDialog drops its removeNodeDB call (and the test
  drops the matching expectation).
- Strip persistNodeDB / VITE_PERSIST_NODE_DB from featureFlags +
  dev-overrides.
- Remove the useNodeDB / useNodeDBStore re-exports + the
  bindStoreToDevice wiring from core/stores/index.ts.

* chore(web): retire Zustand messageStore — keep only Message types/enums

The persisted message Zustand store had no remaining consumers — chat
history, drafts, and message state all live on the SDK ChatClient
(SqlocalMessageRepository). The messageStore Zustand surface
(saveMessage, getMessages, setMessageState, getDraft, setDraft,
clearDraft, deleteAllMessages, clearMessageByMessageId, plus the
addMessageStore / removeMessageStore / getMessageStore / setNodeNum
plumbing) is now fully unused.

Collapse messageStore/index.ts to just the MessageState / MessageType
enums + the legacy `Message` shape that the useChatLegacy adapter and a
couple of message components still consume. Delete the Zustand
implementation and its 32-test suite.

Other knock-on cleanups in this commit:
- Drop the _messageStore param from subscribeAll (unused after the
  saveMessage-from-subscriptions retirement).
- Drop addMessageStore wiring from useConnections, removeMessageStore
  from FactoryResetDeviceDialog, setNodeNum from useNewNodeNum.
- Drop the message branch from the router context (no readers).
- RefreshKeysDialog stops keying off `useMessages().activeChat` (which
  was permanently 0 — a dead handle that quietly broke key-refresh
  UX). Use the SDK NodesClient's first error directly via
  `useNodeErrors()[0]`. The dialog manager already opens the dialog on
  PKI_UNKNOWN_PUBKEY, so picking the first error matches the intended
  flow.

* chore(web): rename useChatLegacy → useChatAsLegacyMessages

The "legacy" suffix on its own read like the hook itself was deprecated;
the actual purpose is "use SDK chat history but project it into the
legacy Message shape." Rename for clarity. Companion type names follow
suit (UseChatAsLegacyMessages{Broadcast,Direct,Params}).

* feat(sdk-react,web): migrate read-only channel consumers to SDK ChannelsClient

Phase one of the channels-slice migration (PR #8 in plan). Moves the
non-changeRegistry-coupled consumers off the legacy deviceStore.channels
Map onto the SDK signal-backed ChannelsClient.

- sdk-react useChannels: switch from useClient to useActiveClient and
  fall back to an empty signal when no client is active. Matches the
  no-throw pattern already used by useNodes / useNodeErrors so isolated
  component tests and pre-connect renders don't throw.
- Messages.tsx reads channels via useChannels(); filteredChannels is
  derived with useMemo, currentChannel via .find(). The SDK Channel
  shape (index/role/settings) matches everything this page touches.
- QRDialog reads channels directly via useChannels(); the channels Map
  prop is dropped from QRDialogProps + DialogManager.
- getChannelName loosens its parameter type to the SDK-compatible
  subset { index, settings?: { name? } } so it accepts both proto
  Channel and SDK Channel without conversion.

Channels.tsx, ImportDialog and Settings/index.tsx still read from the
deviceStore — they are coupled to changeRegistry / setChange and will
move with PR #9 (ConfigEditor).

* feat(sdk,sdk-react): scaffold ConfigEditor for the radio/module/channels edit flow

Replaces the (broken) web changeRegistry with a section-grain editor that
lives on the SDK. Per-connection state, signal-backed, exposed via
client.config.editor.

ConfigEditor (domain):
- baseline   = what the device most recently sent. Updated automatically
  from onConfigPacket / onModuleConfigPacket / onChannelPacket.
- working    = UI edits. setRadioSection / setModuleSection / setChannel.
- dirtyRadioSections / dirtyModuleSections / dirtyChannels signals plus
  an aggregate isDirty signal.
- Inbound baseline updates do NOT discard pending working changes — if a
  section is already dirty, the new baseline is recorded but working stays
  put; dirty bookkeeping recomputes against the updated baseline.
- reset() restores working = baseline.
- commit() wraps beginEditSettings → setConfig per dirty radio variant +
  setModuleConfig per dirty module variant + setChannel per dirty channel
  → commitEditSettings, then optimistically promotes working to baseline.
- Disconnect clears both baseline and working so a stale working copy can
  never leak across reconnects.

Domain types:
- RadioConfig and ModuleConfig are now derived directly from the proto
  Config / ModuleConfig payloadVariant union (Extract<{case: V}> shape) so
  new variants stay typed without manual list maintenance. This adds
  deviceUi (radio) and statusmessage (module) automatically.

sdk-react:
- New useConfigEditor() hook returns the editor for the active client, or
  undefined when no client is active. Components subscribe to the editor's
  signals via the existing useSignal helper.

Tests:
- 5 new ConfigEditor tests covering: clean start, dirty tracking, multi-
  section dirty, reset, mid-edit baseline update preservation, and
  disconnect-clears-state. SDK suite 49 passing.

This commit is scaffolding only — no web settings page rewires yet. The
~25 settings pages move to editor.set* / editor.commit() in follow-up
commits, replacing setChange / commitEditSettings call sites and unblocking
deletion of changeRegistry from the device store.

* feat(web): pilot Position / LoRa / MQTT pages on ConfigEditor; Android-aligned layout + labels

First three settings pages migrated off the legacy changeRegistry to the
SDK ConfigEditor. Section structure, field order, and visible strings
are taken from the Meshtastic-Android source of truth so the web and
mobile UIs converge.

Position (radio config):
- Four cards in Android order: Position Packet → Device GPS → Position
  Flags → Advanced Device GPS.
- Card 1 fields: positionBroadcastSecs, positionBroadcastSmartEnabled,
  broadcastSmartMinimumIntervalSecs (renamed "Smart Interval"),
  broadcastSmartMinimumDistance ("Smart Distance").
- Card 2 fields: fixedPosition, lat/lng/altitude (when fixed), gpsMode
  ("GPS Mode (Physical Hardware)"), gpsUpdateInterval ("GPS Polling
  Interval"). gpsMode + gpsUpdateInterval disable when fixedPosition is
  on; lat/lng/altitude disable when it's off.
- Card 3 fields: positionFlags multiselect (description from Android).
- Card 4 fields: rxGpio ("GPS Receive GPIO"), txGpio ("GPS Transmit
  GPIO"), gpsEnGpio ("GPS EN GPIO").
- onSubmit now calls editor.setRadioSection("position", payload). The
  setFixedPosition admin message path is unchanged.

LoRa (radio config):
- Two cards: Options + Advanced (was three: Mesh / Waveform / Radio).
- Options: region, usePreset, modemPreset (when usePreset),
  bandwidth/spreadFactor/codingRate (when !usePreset).
- Advanced: ignoreMqtt, configOkToMqtt, txEnabled, overrideDutyCycle,
  hopLimit (now "Number of Hops", select 0..7), channelNum,
  sx126xRxBoostedGain ("RX Boosted Gain"), overrideFrequency
  ("Frequency Override"), txPower.
- Drop frequencyOffset (Android does not surface it).
- onSubmit calls editor.setRadioSection("lora", payload).

MQTT (module config):
- Two cards: MQTT Config + Map reporting (new card).
- Card 1: enabled ("MQTT enabled"), address, username, password,
  encryptionEnabled, jsonEnabled, tlsEnabled, root ("Root topic"),
  proxyToClientEnabled.
- Card 2: mapReportingEnabled, mapReportSettings.shouldReportLocation
  (consent gate, "I agree."), positionPrecision (12-15 buckets),
  publishIntervalSecs ("Map reporting interval (seconds)").
- positionPrecision + publishIntervalSecs disable until both
  mapReportingEnabled and shouldReportLocation are true.
- onSubmit calls editor.setModuleSection("mqtt", payload).
- Validation schema gains shouldReportLocation; address/username/
  password limits widened to 63 to match proto, root capped at 31.

Settings/index.tsx handleSave:
- After the legacy changeRegistry flow, runs editor.commit() when the
  editor is dirty. Two beginEdit/commitEdit windows are fine — the
  device handles them sequentially. Once the rest of the settings pages
  migrate, the legacy flow goes away and only editor.commit() remains.
- Save button gating includes editor.isDirty so users can save
  ConfigEditor-only changes (e.g. just a LoRa region tweak).

i18n:
- New section labels (config.json position.{positionPacket,deviceGps,
  advancedDeviceGps}; lora.{optionsCard,advancedCard}; moduleConfig.json
  mqtt.{mqttConfigCard,mapReportingCard}).
- Field labels reworded to match Android strings.xml.
- mqtt.mapReportSettings.shouldReportLocation gains label, description,
  consentHeader, consentText (consent text quoted verbatim from
  Android).

* feat(web): migrate Bluetooth/Device/Display/Power/Network/Security pages to ConfigEditor

Phase 3a of PR #9. Six more radio-config pages off changeRegistry onto
the SDK ConfigEditor. Section structure + field order + labels taken
from Meshtastic-Android source of truth.

Bluetooth: single "Bluetooth Config" card. Drops the legacy disable-
mode-when-disabled rule; mode and PIN remain editable while bluetooth
is off (matches Android).

Device: four cards (Options / Hardware / Time Zone / GPIO).
- Options: role, rebroadcastMode, nodeInfoBroadcastSecs.
- Hardware: doubleTapAsButtonPress, disableTripleClick,
  ledHeartbeatDisabled. Web keeps the proto-aligned (un-inverted) field
  names; Android renders them inverted as "Triple Click Ad Hoc Ping" /
  "LED Heartbeat" — equivalent semantics, simpler binding.
- Time Zone: tzdef.
- GPIO: buttonGpio, buzzerGpio.
- Drops debug-only "Device Storage & UI" card (debug-only on Android).
- Drops the inline "Use phone time zone" action button (queued for a
  follow-up; needs a browser-tz implementation).

Display: two cards (Device Display / Advanced).
- Device Display: compassNorthTop ("Always point north"),
  use12hClock, headingBold, units.
- Advanced: screenOnSecs, autoScreenCarouselSecs, wakeOnTapOrMotion,
  flipScreen, displaymode, oled, compassOrientation (new — was missing
  from web).
- Drops gpsFormat (deprecated in proto; moved to DeviceUIConfig).

Power: single "Power Config" card. Order: isPowerSaving,
onBatteryShutdownAfterSecs, adcMultiplierOverride (web keeps the single
number input; Android splits into a switch + conditional float input —
deferred), waitBluetoothSecs, sdsSecs, minWakeSecs,
deviceBatteryInaAddress. Drops lsSecs (Android does not surface it).

Network: three cards (WiFi Options / Ethernet Options / Advanced).
- WiFi Options: wifiEnabled, wifiSsid, wifiPsk.
- Ethernet Options: ethEnabled.
- Advanced: ntpServer, rsyslogServer, enabledProtocols ("Enabled" /
  forward mesh over UDP), addressMode ("IPv4 mode"), then ipv4 ip /
  gateway / subnet / dns when STATIC.
- Drops the read-only "current connections" card (no analog state on
  web today). IPv4 int↔string conversion is unchanged.

Security: four cards (Direct Message Key / Admin Keys / Logs /
Administration).
- Direct Message Key: privateKey + publicKey (read-only) + Generate +
  Backup buttons.
- Admin Keys: three slots, gated on Legacy Admin channel toggle.
- Logs: serialEnabled, debugLogApiEnabled (Android order: serial
  first).
- Administration: isManaged, adminChannelEnabled.
- Byte-array conversions and the Pki regenerate / managed-mode dialogs
  are unchanged.

i18n: config.json gains card-level keys (bluetoothConfig, options,
hardware, timeZone, gpio, deviceDisplay, advanced, powerConfig,
wifiOptions, ethernetOptionsCard, advancedCard, directMessageKey,
adminKeysCard, logsCard, administration, compassOrientation,
useBrowserTimeZone) and updates field labels to Android strings.xml
values where they differ (e.g. "Bluetooth enabled", "Always point
north", "Use 12h clock format", "Shutdown on power loss", "Wait for
Bluetooth duration", "Battery INA_2XX I2C address", "WiFi enabled",
"Ethernet enabled", "Legacy Admin channel", "Serial console").

* feat(web): migrate 11 module-config pages to ConfigEditor; Android-aligned labels

Phase 3b of PR #9. All remaining module pages off changeRegistry onto
the SDK ConfigEditor. Section structure + field order + labels taken
from Meshtastic-Android source of truth.

Pages migrated (single "<X> Config" card unless noted):

Serial: enabled, echo, rxd ("RX"), txd ("TX"), baud, timeout, mode,
overrideConsoleSerialPort. Drops the per-field disable-when-!enabled
gating (Android keeps fields editable while the module is off).

ExternalNotification: four cards (External Notification Config /
Notifications on message receipt / Notifications on alert/bell receipt
/ Advanced). New card layout matches Android. Schema + page now
include useI2sAsBuzzer. Per-field disable-when-!enabled gating dropped.
Card 1: enabled. Card 2: alertMessage*. Card 3: alertBell*. Card 4:
output, active, outputBuzzer, usePwm, outputVibra, outputMs,
nagTimeout, useI2sAsBuzzer.

StoreForward: adds isServer ("Server"). Removes per-field disable-on-
!enabled. Schema gains isServer.

RangeTest: relabel to Android strings ("Range test enabled", "Sender
message interval (seconds)", "Save .CSV in storage (ESP32 only)").
Removes per-field disable-on-!enabled.

Telemetry: adds deviceTelemetryEnabled ("Send Device Telemetry" — gated
on firmware ≥ v2.7.12 capability flag on Android; web shows
unconditionally for now) and powerScreenEnabled ("Power metrics on-
screen enabled"). Schema gains both. Field labels updated.

CannedMessage: relabels per Android ("Canned message enabled", "GPIO
pin for rotary encoder A/B/Press port", "Generate input event on
Press/CW/CCW", "Up/Down/Select input enabled", "Allow input source",
"Send bell"). The free-form `messages` text field is a separate admin
RPC (setCannedMessages on Android) — queued as a follow-up.

Audio: relabel ("CODEC 2 enabled", "PTT pin", "CODEC2 sample rate",
"I2S word select / data in / data out / clock").

NeighborInfo: adds transmitOverLora toggle. Schema gains it.

AmbientLighting: relabel ("Ambient Lighting Config" card). Same fields.

DetectionSensor: relabel + reorder per Android (enabled,
minimumBroadcastSecs, stateBroadcastSecs, sendBell, name, monitorPin,
detectionTriggerType, usePullup).

Paxcounter: relabel ("Paxcounter enabled", "Update interval", "WiFi
RSSI threshold (defaults to -80)", "BLE RSSI threshold").

i18n: moduleConfig.json gains card-level keys (serialConfig,
externalNotificationConfig, notificationsOnMessage, notificationsOnAlert,
advanced, storeForwardConfig, rangeTestConfig, telemetryConfig,
cannedMessageConfig, audioConfig, neighborInfoConfig,
ambientLightingConfig, detectionSensorConfig, paxcounterConfig) and
new field keys (deviceTelemetryEnabled, powerScreenEnabled,
transmitOverLora, isServer, useI2sAsBuzzer, minimumBroadcastSecs,
stateBroadcastSecs, sendBell, name, monitorPin, detectionTriggerType,
usePullup), with field labels reworked to match Android strings.xml
values.

The User module page is the last consumer of changeRegistry. After it
moves, the deviceStore changeRegistry plumbing + getEffectiveConfig +
setChange / removeChange / getAllConfigChanges / etc. can be deleted.

* feat(sdk,web): migrate User + Channel + ImportDialog off changeRegistry; ConfigEditor gains owner support

Phase 3c-1 of PR #9. The last writers/readers of changeRegistry are
moved to the SDK ConfigEditor. (One sweep commit drops the
changeRegistry plumbing from the deviceStore.)

ConfigEditor (sdk/features/config/domain/ConfigEditor.ts):
- New owner state: baselineOwner / workingOwner signals, isOwnerDirty,
  setBaselineOwner(user), setOwner(user). The editor doesn't subscribe
  to a SDK signal for the user (owner arrives via NodeInfo, not its own
  packet), so the web seeds baseline from useMyNodeAsProto via setter.
- commit() now also runs setOwner (admin message) when isOwnerDirty,
  inside the same beginEdit/commitEdit window as radio/module/channels.
- isDirty aggregates owner dirty too. reset() reverts working owner.
- Disconnect clears baseline + working owner.

User page (Settings/User.tsx):
- Aligned to Android: single "User Config" card, fields in order:
  Node ID (read-only), Long Name, Short Name, Hardware model
  (read-only enum name), Unmessageable, Licensed amateur radio (Ham).
- onSubmit calls editor.setOwner instead of connection.setOwner —
  edits queue with the rest of the editor's pending changes and ship
  on the global Save.
- Preserves non-edited User fields (id, hwModel, role, publicKey,
  macaddr) by spreading the baseline before applying form deltas.
- isUnmessageable now reads from the proto's isUnmessagable instead of
  defaulting to false (longstanding bug — the existing value was
  ignored).
- shortName min loosened to 1 (was 2) per Android rule.

Channel page (PageComponents/Channels/Channel.tsx):
- Reads working channel from editor.channels via useSignal projection;
  drops getChange / setChange / removeChange / deepCompareConfig.
- onSubmit -> editor.setChannel(payload).

Channels page (PageComponents/Channels/Channels.tsx):
- Reads channel list from useChannels() (SDK), maps each into a real
  proto Channel via create(ChannelSchema, ...) for the per-tab Channel
  component which still expects the proto shape.
- Per-channel dirty indicator now reads editor.dirtyChannels instead
  of hasChannelChange.

ImportDialog (Dialog/ImportDialog.tsx):
- Apply path -> editor.setChannel + editor.setRadioSection("lora",...)
  instead of setChange. Channel reads via SDK.

i18n: config.json user.* gains userConfig (card label), nodeId,
hardwareModel keys; field labels reworded to match Android (e.g.
"Licensed amateur radio (Ham)", "Unmessageable" with "Unmonitored or
Infrastructure" description). UserValidationSchema gains optional
nodeId / hardwareModel display fields.

* feat(sdk,web): delete changeRegistry; ConfigEditor owns all settings dirty state

Phase 3c-2 of PR #9. With every web settings page already migrated, the
deviceStore changeRegistry plumbing has no remaining writers or readers
and is deleted.

ConfigEditor (sdk):
- New queueAdminMessage(message) for one-off side flows. Queue drains
  inside commit() between beginEdit and commitEdit. Used by
  Position.tsx for setFixedPosition; future side flows (e.g. SetTime)
  reuse the same queue.
- Disconnect / commit success now also reset the admin-message queue.
- isDirty includes a queued admin-message check.

Web Settings/index.tsx:
- handleSave is now a one-liner: editor.commit(). All the legacy
  get*Changes / get*ChangeCount / clearAllChanges / connection.setConfig
  loops + the post-commit deviceStore mirror writes are gone.
- handleReset calls editor.reset() instead of clearAllChanges().
- Save-button gating reads editor.isDirty + RHF dirty.
- Section change-count badges read directly from editor.dirtyRadioSections /
  editor.dirtyModuleSections / editor.dirtyChannels lengths.

Tab dirty-dot indicators (RadioConfig.tsx / DeviceConfig.tsx /
ModuleConfig.tsx): rewired from hasConfigChange/hasModuleConfigChange/
hasChannelChange/hasUserChange to editor signals (dirtyRadioSections,
dirtyModuleSections, dirtyChannels, isOwnerDirty).

Position.tsx: queueAdminMessage call now goes through editor — pending
fixed-position coordinates ride along with the rest of the editor's
dirty state and ship under the same beginEdit/commitEdit window.

deviceStore.ts:
- Drops the entire changeRegistry surface from the Device interface and
  factory: setChange / removeChange / hasChange / getChange /
  clearAllChanges / hasConfigChange / hasModuleConfigChange /
  hasChannelChange / hasUserChange / getConfigChangeCount /
  getModuleConfigChangeCount / getChannelChangeCount /
  getAdminMessageChangeCount / getAllConfigChanges /
  getAllModuleConfigChanges / getAllChannelChanges / queueAdminMessage /
  getAllQueuedAdminMessages, plus the changeRegistry field.
- getEffectiveConfig / getEffectiveModuleConfig collapse to plain
  device.config[variant] / device.moduleConfig[variant] passthroughs
  (kept as a thin compatibility shim for residual callers; the
  changeRegistry merge is gone).
- types.ts: ValidConfigType / ValidModuleConfigType derive directly
  from LocalConfig / LocalModuleConfig keys (was imported from the
  deleted changeRegistry.ts).
- The 252-line changeRegistry.ts file is deleted; ~250 lines also drop
  out of the deviceStore factory.
- deviceStore.test.ts loses the change-registry describe block (3
  tests). 237 web tests still passing (was 240; -3 dead tests).

Net: -1066 / +136 lines.

* feat(sdk,sdk-storage-sqlocal,web): TelemetryRepository port + SqlocalTelemetryRepository (PR #10)

Telemetry slice gains the same persistence shape as chat / nodes: a
repository port on the SDK side, an in-memory default, an OPFS-backed
SQLite adapter on the storage package side, and lazy hydration into the
in-memory store.

SDK (@meshtastic/sdk):
- New TelemetryRepository port (loadRecent, loadBefore, append,
  appendBatch, prune, clearNode, clear) + TelemetryRetentionPolicy
  (maxPerNode, olderThanMs).
- New InMemoryTelemetryRepository — default when no adapter is wired.
- TelemetryClient grows TelemetryClientOptions { repository?, retention? }.
  Each incoming onTelemetryPacket appends to both the in-memory store
  and the repository, then the configured retention policy prunes the
  repository.
- latest(nodeNum) and history(nodeNum) lazy-hydrate the in-memory store
  from the repository on first subscribe (HYDRATE_LIMIT = 256). loadBefore
  passes through to the repository for paged reads.
- clearNode / clear methods exposed on the client.
- MeshClient gains options.telemetry which is passed through to
  TelemetryClient.

@meshtastic/sdk-storage-sqlocal:
- New SqlocalTelemetryRepository under ./telemetry subpath. Drizzle-typed
  insert/select/delete against the existing telemetry table; payload is
  stored as base64 of the proto bytes (per-kind schema lookup) so the
  wire shape is the source of truth across schema additions.
- prune({ maxPerNode }) trims with a per-node offset query. prune({
  olderThanMs }) deletes by ts cutoff.
- Six new tests cover round-trip, ascending order, deviceId scoping,
  per-node retention, age-based prune, and proto payload preservation.
- Re-exports added to mod.ts + ./telemetry subpath in package.json.

web:
- useConnections wires SqlocalTelemetryRepository per connection with
  retention { maxPerNode: 500, olderThanMs: 30 days }.

Tests: SDK 57 (was 49 — +8 new), storage 30 (was 24 — +6), web 237.
Build clean.

* feat(sdk,sdk-react,web): unread counts on the SDK ChatClient

ChatClient gains a `chat.unread` namespace mirroring `chat.drafts`:
- byKey: ReadonlySignal<Map<conversation-key-string, number>>
- total: ReadonlySignal<number>
- count(key): ReadonlySignal<number>
- markRead(key): void

Increments happen in the existing onMessagePacket subscriber when
packet.from !== client.myNodeNum (so the outbound echo of our own send
doesn't bump anything). Uses the same ConversationKey shape the rest
of the chat slice already keys by, so direct messages keyed by peer
and broadcasts by channel can never collide.

Counts are in-memory only — persistence will come later if needed
(would require tracking a lastReadAt timestamp per conversation in the
message repository and computing unread on hydrate).

sdk-react: new useTotalUnread / useUnreadCount(key) / useUnreadByKey
hooks. All fall back to safe empty values when no client is active.

Web migration:
- Sidebar reads useTotalUnread instead of summing
  deviceStore.unreadCounts.
- MessagesPage: per-channel SidebarButton counts read from
  useUnreadByKey()'s `channel:N` keys; per-direct-message counts from
  `direct:N` keys. Click handlers call meshClient.chat.unread.markRead
  with the typed ConversationKey instead of resetUnread.
- subscriptions.ts: drops onMessagePacket -> incrementUnread mirror
  (SDK now owns it). myNodeNum local goes away too — nothing else
  consumed it.
- deviceStore: removes unreadCounts field + incrementUnread /
  resetUnread / getUnreadCount / getAllUnreadCount methods. The mock
  + test block in deviceStore.test.ts also drop their unread coverage.
- deviceStore.mock.ts: also strips the dead changeRegistry / setChange
  / hasConfigChange / etc. methods that survived from the earlier
  changeRegistry deletion.

Tests: SDK 61 -> 65 (+4 ChatClient.unread.test), sdk-react 8, web 236.
Build clean.

* refactor(web): split useConnections into focused modules

useConnections went from 661 lines down to 264 by extracting three
single-responsibility helpers under packages/web/src/core/connections/.
The hook is now pure orchestration; transport / heartbeat / SDK-client
/ status-probe concerns live in their own files.

New modules:
- core/connections/heartbeat.ts (45 LOC): startConfigHeartbeat (5s),
  startMaintenanceHeartbeat (5min), stopHeartbeat. Owns the heartbeats
  Map; replaces the inline interval bookkeeping. Both helpers stop the
  prior heartbeat first so callers don't have to.
- core/connections/sdkClient.ts (61 LOC): buildMeshDevice(connId,
  deviceId, transport) opens the OPFS DB, builds the four sqlocal
  repositories (chat / draft / nodes / telemetry), and constructs the
  MeshDevice with the canonical retention defaults (chat 90d / 1k per
  bucket; telemetry 30d / 500 per node). Falls back to in-memory when
  sqlocal is unavailable.
- core/connections/transports.ts (236 LOC): per-transport openTransport
  factory (HTTP reachability check, BT permission re-acquisition with
  optional prompt, Serial port lookup with close-then-reopen),
  probeConnection for refreshStatuses, closeTransport for cleanup.
  Discriminates over conn.type so useConnections doesn't carry the
  switch logic.

useConnections (now 264 LOC):
- Owns the cachedTransports + configSubscriptions maps and the
  Zustand selectors.
- teardown(id, conn) consolidates the heartbeat + config-sub +
  meshDevice.disconnect + transport-close cleanup that used to be
  duplicated across removeConnection / disconnect.
- connect calls openTransport and forwards the resulting transport +
  cached BT/Serial handle into setupMeshDevice. The BT
  gattserverdisconnected listener is wired here (it needs the connId).
- refreshStatuses simplifies to a filter + Promise.all over
  probeConnection.
- syncConnectionStatuses unchanged.

Behavior is unchanged. No new tests — the existing web suite (236)
still passes; build clean.

* chore: move packages/web → apps/web

Web is a deployable SPA, not a library — no @meshtastic scope, no
exports map, no npm/JSR publish target, no other workspace member
depends on it. Conventional pnpm/Turbo/Nx layout puts deployables
under apps/ and libraries under packages/. Doing the move now (after
PR #9 / #10 / unread / useConnections refactor land but before Phase
C ships) so the lib-only packages/* directory remains stable as we
delete packages/core.

Mechanics:
- pnpm-workspace.yaml: add `apps/*` to packages.
- git mv packages/web apps/web (entire directory tree).
- vite.config.ts uses process.cwd(), so no internal path edits needed.
- pnpm filter commands still resolve by package name (`pnpm --filter
  meshtastic-web run build`) — no script changes.

External-path consumers updated:
- README.md table row.
- tsconfig.json reference.
- 7 GH workflows (pr.yml, nightly.yml, release-web.yml, the three
  crowdin-*.yml, release-packages.yml). The
  packages/release-packages.yml `packages/* | grep -v packages/web`
  filter is now redundant since web isn't in packages/* anymore — it
  collapses to plain `ls -d packages/*`.

Verified: web build clean, web 236 / sdk 65 / sdk-react 8 / storage
30 tests all pass.

* feat: PR #12 Phase C — delete packages/core, retarget transports to @meshtastic/sdk, bump majors

The legacy `@meshtastic/core` package is gone. The six transport-*
packages and the web app no longer depend on it; everything routes
through `@meshtastic/sdk`.

Transport packages (transport-deno, transport-http, transport-node,
transport-node-serial, transport-web-bluetooth, transport-web-serial):
- package.json deps swap `@meshtastic/core: workspace:*` for
  `@meshtastic/sdk: workspace:*`.
- src/transport.ts + src/transport.test.ts imports point at
  `@meshtastic/sdk` (the `Types` and `Utils` namespaces are still
  exported from the SDK so source code is otherwise unchanged).
- README.md examples updated to import from `@meshtastic/sdk`.

apps/web:
- Drops the leftover `@meshtastic/core` workspace dep; nothing in
  apps/web/src imports from it.

@meshtastic/sdk:
- README description loses the "Replaces @meshtastic/core" suffix —
  there's nothing left to replace.
- Three legacy shim files keep their re-exports but their docstrings
  drop the "Phase-A shim, removed in Phase C" framing. The MeshDevice
  facade survives because the web app's `connection.factoryResetDevice()`
  / `connection.reboot()` / etc. callsites still go through it; new
  consumers should reach into `client.config` / `client.chat` / etc.
- New scripts/rename-dts.mjs is a postbuild step that renames tsdown's
  hashed entry-point dts outputs (`mod-<hash>.d.ts` etc.) back to their
  canonical names so package.json `types` and downstream dts-bundlers
  can find them. Internal chunk dts files (e.g. `Transport-<hash>.d.ts`)
  are intentionally NOT renamed because mod.d.ts imports them by path.
- build:npm runs tsdown then the rename script.

Version bumps (signal: now require @meshtastic/sdk):
- @meshtastic/sdk             0.1.0 -> 1.0.0
- @meshtastic/sdk-react       0.1.0 -> 1.0.0
- @meshtastic/sdk-storage-sqlocal 0.1.0 -> 1.0.0
- @meshtastic/transport-http       0.2.5 -> 1.0.0
- @meshtastic/transport-web-serial 0.2.5 -> 1.0.0
- @meshtastic/transport-web-bluetooth 0.1.5 -> 1.0.0
- @meshtastic/transport-deno       0.1.1 -> 1.0.0
- @meshtastic/transport-node       0.0.2 -> 1.0.0
- @meshtastic/transport-node-serial 0.0.2 -> 1.0.0

Workflows: nothing referenced packages/core directly (only the
release-packages.yml glob which already collapsed in the apps/web
move). README packages table loses the legacy `packages/core` row.

Verified: web build clean, all four package suites green
(web 236, sdk 65, sdk-react 8, storage 30).

The transport packages' dist build is currently blocked by a tsdown
cross-chunk dts resolution glitch where the `Types` namespace import
can't follow `mod.d.ts`'s reference to the internal `Transport-<hash>`
chunk. Runtime is fine — only the published-types path is affected,
and transport packages publish via their own release flow. Logged as
follow-up.

* fix(web/connections): probe = "online" not "configured"; flip status to "configuring" before DB open; await transport disconnect

Three reconnect-flow bugs:

1. probeConnection returned "configured" for serial / bluetooth when
   the browser had stored permission for the device — but permission
   ≠ a configured Meshtastic device. The card showed "connected"
   and a Disconnect button before the user had ever clicked Connect.
   Probe now returns "online" (= "available, click to connect"),
   matching the HTTP path. "configured" is reserved for the
   onConfigComplete callback.

2. setupMeshDevice flipped status from "connecting" → "configuring"
   AFTER awaiting buildMeshDevice, which opens the OPFS DB. If the
   DB open stalled (multi-tab contention, slow first-time init), the
   card sat at "connecting" indefinitely. Move the status flip ahead
   of the persistence await — UI shows "configuring" while
   persistence is spinning up.

3. teardown's `device?.connection?.disconnect()` returned a Promise
   that was never awaited, so the underlying transport's port.close()
   could race the next port.open() on a fast disconnect → reconnect.
   Make teardown async, await disconnect, propagate via async
   removeConnection / disconnect callers.

Plus: connect() catch block logs the underlying error before turning
it into a status update, so the actual failure shows up in console
even when the toast text is generic.

* feat(web): Position "Use browser location" button + Telemetry firmware capability gate

Two backlog UX nits.

DynamicForm:
- FormGroup gains an optional `footer?: ReactNode` slot that renders
  after the field list. Lets pages drop arbitrary JSX (action buttons,
  notes, etc.) into a card without subclassing the form.

Position:
- New `UseBrowserLocationButton` component lives inside the Device GPS
  card via the new `footer` slot. Calls navigator.geolocation, writes
  lat/lng/altitude into the form via `useFormContext` (the
  PositionValidation form is wrapped in FormProvider by DynamicForm).
- Truncates lat/lng to 7 decimals to match the proto's max precision.
- Gracefully no-ops when navigator.geolocation is unavailable
  (insecure context).
- Failed reads surface as a toast carrying the GeolocationPositionError
  message; busy state disables the button + flips the label.
- New i18n keys: position.useBrowserLocation.{label,busy,failed}.

Telemetry:
- `device_telemetry_enabled` is only writable on firmware ≥ v2.7.12
  (mirrors Android's `Capabilities.canToggleTelemetryEnabled`). Adds a
  small `firmwareAtLeast` semver helper and reads
  `device.metadata.get(0)?.firmwareVersion`. The toggle is hidden on
  older firmware so we don't push a value the device will silently
  drop.
- Unparseable / unknown firmware versions default to "show the toggle"
  (rather than hide), so we don't accidentally hide the control on
  devices we can't classify.

Build clean, 236 web tests still pass.

* fix(sdk): build clean dts for npm consumers; transports drop Types/Utils namespace

Previously transport-* package dts builds failed with "Missing export" because
rolldown's dts bundler couldn't follow tsdown's cross-chunk re-exports out of
@meshtastic/sdk. Three things land here:

1. mod.ts now re-exports `fromDeviceStream`, `toDeviceStream`, `Queue`, and
   `Xmodem` directly. Transport packages were importing these via the legacy
   `Utils` namespace; namespace re-exports are exactly what rolldown chokes
   on. Transport packages (http, deno, node, node-serial, web-bluetooth,
   web-serial) and shared transportContract.ts now use direct imports —
   `import { Transport, DeviceOutput, ... } from "@meshtastic/sdk"`.

2. package.json `exports` split: `types` points at `./dist/<entry>.d.ts`
   (built dts) so tsdown's downstream rolldown reads a self-contained file;
   `default` keeps source paths so vitest + workspace-internal imports still
   resolve to .ts source.

3. Postbuild `rename-dts.mjs` now (a) inlines internal chunk dts files
   (Transport-<hash>.d.ts) into each entry's dts, rewriting the letter
   aliases tsdown produces; (b) strips `type` modifiers from re-exports
   so rolldown accepts the bindings as both value- and type-shape;
   (c) declares the synthetic `Types`/`Utils` namespaces explicitly,
   replacing the broken `<wrapper>_d_exports as Types` aliases tsdown
   emits for `export * as` patterns.

vitest.config.ts gains `apps/*` so the workspace move keeps test discovery
working for the apps/web project.

* chore(build): tighten tsdown perf flags across SDK + transport packages

- Drop bogus `dts.isolatedDeclarations` field from SDK (not a real tsdown
  option; tsdown was silently ignoring it).
- Make every package explicit about the perf-relevant flags rather than
  relying on defaults that drift between tsdown versions: target=esnext
  (no syntax downleveling), sourcemap=false, minify=false, treeshake=true,
  report=false, splitting=false.
- Modest wall-clock wins: SDK 1020→752ms, transports ~720→~600ms.

`isolatedDeclarations: true` in tsconfig would unlock the bigger oxc-dts
fast path but the codebase isn't ID-clean yet — left as future work.

* fix(web): clear pre-existing typecheck errors (36 → 0)

Sweep covering several unrelated cohorts:

- Delete dead `NewDeviceDialog.tsx` (commented-out in App.tsx, imports point at
  non-existent `@components/PageComponents/Connect/*`).
- Add `better-result` to sdk-react deps so `useChat`/`useDirectChat`/
  `useFavoriteNode`/`useIgnoreNode` resolve.
- Fix `test-utils.tsx`: route at `routeTree.gen.ts` was never generated;
  re-export the existing `routeTree` constant from `routes.tsx` and pass
  the missing router context + DeviceWrapper deviceId.
- Pre-existing `noUncheckedIndexedAccess` violations in `pskSchema.test.ts`,
  `x25519.ts`, `Table/index.tsx` + test, `useCopyToClipboard` (Timeout vs
  number) — non-null asserts where the index is guaranteed by the
  surrounding code, plus a defensive `if (!cell) return 0` in the table sort.
- Immer `WritableDraft<T>` mismatches against SDK's `MeshDevice`/`Device`
  classes — cast through `Draft<X>` where the inferred recursive draft type
  can't represent SDK class internals.
- `deviceStore.mock` updated for the `connectionPhase`/`connectionId` +
  setters added when the connection lifecycle was extracted.
- `HeatmapLayer` accepts the `isVisible` prop everyone else's layer accepts.
- `SNRTooltipProps.to` widened to `string | undefined` (single-node hover
  has no `to`).
- Zod resolver returns `Record<string, never>` for the error-path values
  shape that react-hook-form expects.
- Mock `NodeInfo` in `useFilterNode.test.ts` gains the `isMuted` proto
  field that landed upstream.

* fix(web/connections): retry-once + clearer error on Bluetooth GATT connect failure

`NetworkError: Connection attempt failed` from `device.gatt.connect()` is the
most common transient when reconnecting BT — OS stack hiccup, device just
woke from sleep, etc. Adding one retry with a 750ms delay clears most of
those. Persistent failures get a wrapped error message naming the actual
likely causes (out of range, powered off, paired with phone or another
tab) instead of leaving the raw DOMException for the user to interpret.

* refactor(transport-web-bluetooth): own GATT connect-retry semantics + typed error

Per the DDD layering — transport-level concerns belong in the transport
package, not the web app's connection orchestrator. Moved the GATT
`NetworkError: Connection attempt failed` retry-once + the user-facing
error wrapping out of `apps/web/src/core/connections/transports.ts` and
into `TransportWebBluetooth.prepareConnection`.

New `BluetoothConnectError` carries:
- `kind: "transient" | "unavailable" | "missing-service"` so callers can
  decide whether to suggest retry vs re-pair vs firmware upgrade.
- `userMessage` — human-readable, actionable string ready for UI.

The web app's `transports.ts` is back to a thin wrapper that just calls
`TransportWebBluetooth.createFromDevice` and surfaces whatever error it
throws. `BluetoothConnectError` is re-exported through the connections
module so the Connections page can `instanceof`-check without depending
on the transport package directly.

* feat(sdk,web): detect newly-flashed device + prompt region setup

Mirrors Meshtastic-Android's `regionUnset` flow. A freshly-flashed device
boots with `Config.LoRa.region == UNSET` and won't transmit at all until
the user picks one. Web now surfaces this:

- `ConfigClient.isRegionUnset: ReadonlySignal<boolean>` — computed from
  the radio config signal, false until the first LoRa packet arrives so
  consumers don't flash the prompt during the connect handshake.
- `useIsRegionUnset()` hook in sdk-react, re-exported from the package's
  public surface.
- `RegionSetupReminder` mounts inside the connected-device branch of
  App.tsx. While the SDK reports `isRegionUnset == true`, a persistent
  toast offers a "Set region" CTA that deep-links into the LoRa tab
  (`/settings/radio`). Toast auto-dismisses the moment a real region is
  committed.

Predicate matches Android exactly (single check, no owner/name/setupComplete
heuristics). Lives in the SDK config slice — UI is a thin consumer.

3 new SDK tests; SDK 64 / sdk-react 8 / web 236.

* refactor(transport-web-serial): own port-state hygiene + Result-typed connect errors

Pre-flight close + open-with-backoff lived in apps/web's transport
orchestrator — wrong layer per DDD. Lifted into TransportWebSerial
itself.

Changes:
- `TransportWebSerial.createFromPort` (and `create`) now return
  `Result<TransportWebSerial, SerialConnectError>`. No throwing on
  expected failures; callers branch on the Result.
- New `preparePort` private static that:
  * force-closes the port if `readable` / `writable` are non-null
  * opens with up to 4 attempts (250 / 500 / 750 ms backoff) on
    `InvalidStateError` — common during USB re-enumeration
  * tags persistent failures `kind: "in-use"` (other tab / app
    holding the port) vs `"busy"` (transient / unknown) vs
    `"unavailable"` (no streams after open)
- `SerialConnectError` carries `kind` + `userMessage` ready for UI.
- `apps/web/src/core/connections/transports.ts` drops its old
  conditional `port.close()` + 100ms sleep; just calls
  `createFromPort` and unwraps the Result. `SerialConnectError` is
  re-exported alongside `BluetoothConnectError` so the Connections
  page can pattern-match.
- `AnyTransport` definition simplified — was deriving via
  `Awaited<ReturnType<...>>` which broke once `createFromPort`
  returned a Result.
- Test FakeSerialPort updated to mirror the real spec: `readable`
  /`writable` go null on `close()` and re-create on `open()`.
- 3 new port-hygiene tests cover the close-on-prior-open path,
  the backoff-retry success path, and the persistent-`in-use`
  Err path. Suite: 5 → 8.

Matches the better-result preference for new application/use-case
code. BT transport stays on the throwing `BluetoothConnectError`
shape for now — separate refactor if we want to converge.

* chore(logging): tslog-gated debug logs across the serial connect path

createLogger now reads `localStorage["mesh-debug"]==="1"` (browser) or
`MESH_DEBUG=1` (node) on each invocation. When set, default minLevel
drops to debug; otherwise stays at info. Existing callers
(MeshClient + co.) are unaffected.

Logs added (all `[name]`-prefixed via tslog) covering the connect
lifecycle the user just hit:

- TransportWebSerial.preparePort: enter, close-needed branch,
  close result, each open() attempt, retry decisions, final error.
- TransportWebSerial constructor: pipe wiring, toDevice pipe abort
  vs reject path.
- TransportWebSerial read loop: start, done, throw (with
  closingByUser flag), reader-lock release.
- TransportWebSerial.disconnect: each cleanup step (abort, pipe
  settled, fromDevice cancel, connection.close).
- transports.ts openSerial: cached-port flag, getPorts count,
  picker invocation, resolved-port stream state, createFromPort
  Result outcome (kind + userMessage on Err).
- useConnections.connect / setupMeshDevice / teardown: phase
  transitions, configure() resolve / reject, onConfigComplete fire,
  BT gattserverdisconnected, transport.disconnect rejection.

Flip on:
  localStorage.setItem("mesh-debug", "1"); location.reload();

* chore(web/storage): logs + 5s timeout around sqlocal DB open

The connect path silently hung at `buildMeshDevice` when sqlocal failed
to open. Adding visibility:

- `getStorageDb` logs creation start, success (with elapsed ms), and
  failure (with name + message).
- `buildMeshDevice` races the DB open against a 5s timeout. If sqlocal
  hangs (stale Web Lock from a crashed prior tab, OPFS contention,
  worker hang), the connect path falls through to the SDK's in-memory
  repositories instead of blocking forever — the user gets a working
  session and the warn-level log surfaces the underlying cause.
- `buildMeshDevice` logs entry, DB-ready elapsed, repos-opened, and
  fall-through decisions.

* feat(sdk,web): live connect-progress overlay (terminal-style log)

Surfaces what the connection handshake is doing in real time. Per DDD,
the predicate + counters live in the SDK; the web overlay is a thin
consumer of the signal.

SDK
- New `MeshClient.progress: ReadonlySignal<ConnectionProgress>` with the
  state machine `idle → configuring → configured`. Counters
  (config / modules / channels / nodes + boolean myInfo / metadata)
  tally inbound packets each `configure()` cycle.
- `configure()` resets to `configuring` with empty counters.
  `onConfigComplete` flips to `configured`.
- `ConnectionProgress` + `ConnectionProgressCounters` exported from the
  package surface.
- 6 new tests in `MeshClient.progress.test.ts`.

sdk-react
- `useConnectionProgress()` hook reads the active client's signal,
  returns `{ phase: "idle" }` when there is no active client so the
  caller can render unconditionally.

apps/web
- `ConnectingOverlay` redesigned as a terminal-style streaming log:
  - macOS-window-style header (red/yellow/green dots) with a live
    `phase` indicator pulse.
  - Monospace event lines with `→` (active) / `✓` (done) / `•` (info)
    glyphs and a relative-seconds timestamp column.
  - Auto-scrolls to the latest entry; blinking caret on the trailing
    active line.
  - Lines derive from `device.connectionPhase` transitions and per-
    counter bumps on `MeshClient.progress`. Resets on each fresh
    connect cycle.
- Mounted at the top of the device tree (outside the device-conditional
  branch) so it shows during a first-time connect from the Connections
  screen as well as reconnects from inside the app.
- i18n strings under `connections.overlay.log.*`.

* refactor(web): redesign ConnectingOverlay as hero card + fix early visibility

Two changes:

Visibility fix — overlay now reads `savedConnections` for any entry
whose status is "connecting" or "configuring". The previous code keyed
off `device.connectionPhase` from the active deviceStore entry, but
that entry doesn't exist until partway through `setupMeshDevice` —
the overlay was hidden during transport-open + storage-open (often
the slowest part of the connect flow).

Visual redo (option C — hero card with radial pings):
- Center: transport-type icon (Globe / Bluetooth / Cable) on a
  gradient disc with two concentric ping rings.
- Below: device name + phase label with small spinner.
- 2x2 grid of stat chips (Identity / Metadata / Channels / Nodes)
  that flip from gray (dashed circle) to emerald (check or count)
  as the SDK reports each piece arriving.
- Slate-to-black gradient panel; non-dismissable while active.
- i18n strings under `connections.overlay.*` updated to match.

* fix(web/connecting-overlay): clear stale in-flight statuses + Cancel escape

Two reasons the overlay could "never disappear":

1. Persisted state from a prior session.
   `savedConnections` is Zustand-persisted, so a prior tab that died
   mid-connect (page reload, hot reload, force-quit) leaves a saved
   connection at status "connecting" / "configuring" forever. On cold
   boot the overlay reads that and stays visible. `onRehydrateStorage`
   now sweeps any persisted "connecting" / "configuring" /
   "disconnecting" entries back to "disconnected" — no live JS code
   could complete those, so they're stale by definition.

2. Live attempt that genuinely hangs (firmware in CLI / bootloader,
   framing out of sync, `onConfigComplete` never arrives).
   After STUCK_THRESHOLD_MS (15s) the overlay surfaces a Cancel button
   + a hint. Cancel calls `useConnections().disconnect(id)` which tears
   down the transport and flips status off "configuring". Resets if a
   new attempt starts.

i18n adds `connections.overlay.stuckHint` + `cancel`.

* fix(web/connections): close configured-transition race when configCompleteId is buffered

Reproed on serial reconnect: the device kept emitting from a prior
session, so the new MeshClient's `transport.fromDevice → decodePacket`
pump processed the buffered `configCompleteId` synchronously inside
the `buildMeshDevice` await — before `setupMeshDevice` had a chance to
subscribe to `onConfigComplete`. SimpleEventDispatcher doesn't replay
to late subscribers, so the event was lost. The connect overlay (which
keys off the saved-connection status) stayed visible forever even
though the rest of the app saw nodes / messages flowing.

Fix: wire two parallel paths to a shared idempotent `markConfigured`
handler:

- `events.onConfigComplete` (fast path, fires on the next live event).
- `MeshClient.device.status` signal — `decodePacket` also flips the
  signal to `DeviceConfigured` on the same packet, and signal
  subscribers don't drop the current value if it's already there.

Plus an initial synchronous check in case the status signal had
already settled before either subscribe ran. `configSubscriptions`
unsubscribes both on teardown.

* style(connecting-overlay): drop hardcoded slate/sky for the app's theme tokens

Reuses the same `--color-text-primary`, `--color-text-secondary`,
`--color-background-primary`, and `--color-link` tokens the rest of
the web UI already binds to (light + dark variants resolve via
`[data-theme=...]`), so the overlay now picks up the active theme
instead of fighting it with a hardcoded slate/sky/indigo palette.

- Hero ring + disc → `bg-link` + `bg-link/{20,30}` ping ripples.
  Icon uses `text-background-primary` so it stays legible on both
  themes (white-on-blue light, dark-on-blue dark).
- Title / phase / hint copy → `text-text-primary` / `text-text-secondary`.
- Stat chips use `green-500` (matches `Button.success`) for the done
  state and `text-secondary`-derived neutrals for idle.
- Cancel button reverts to the default `Button` outline variant — was
  carrying its own slate overrides, no need.

* fix(sdk/chat): optimistic-append outbound text so it shows up after send

Reproed in MessagesPage on /messages/broadcast/0: typing a message and
pressing Enter cleared the input, but the message never rendered.

Root cause:
- `ChatClient.send` calls `sendText` → `MeshClient.sendPacket(...,
  echoResponse=true)`. The echo branch dispatches `onMeshPacket` only.
- `ChatClient` subscribes to the per-portnum `onMessagePacket`, not the
  raw `onMeshPacket`. So the outbound message is never appended to its
  conversation bucket.
- The Meshtastic firmware does not loop the user's own outbound text
  back via `fromradio`, so there's no inbound echo to fall back on.

Fix:
- `ChatClient.send` now optimistically appends the outbound message
  (state=Pending) to the matching channel/direct bucket and persists
  it via the configured repository as soon as `sendText` resolves Ok.
- The existing `onRoutingPacket` subscriber flips state Pending→Ack /
  Failed when the routing ack arrives keyed by packet id — unchanged.
- `ChatStore.hasMessage(key, id)` added; the inbound `onMessagePacket`
  subscriber and the `send()` path both consult it so a
  belt-and-suspenders firmware echo doesn't double-append.

Tests: ChatClient.send.test.ts — 4 cases (broadcast append, direct
append, echo dedup, routing-ack flips state). Suite 70 → 74 / web
suite untouched at 236.

* fix(sdk/chat): append outbound message before awaiting send (instant render)

Previously the optimistic append happened after `await sendText(...)`,
which itself awaited `queue.wait(id)` — i.e. the firmware ack. On LoRa
that's 1–3 seconds, so the user saw a noticeable delay between hitting
Enter and the bubble appearing.

Move the append before the await. To do that the chat slice needs to
know the packet id synchronously, so:

- `MeshClient.sendPacket` gains an optional `packetId` parameter
  (defaults to `generatePacketId()`, behaviour unchanged for callers
  that don't pass one).
- `SendTextInput.packetId` plumbs through `SendTextUseCase` to
  `sendPacket`.
- `ChatClient.send` generates the id up-front, appends the message
  with `state=Pending` immediately, persists it, then awaits
  `sendText(..., { packetId })`. On Err the optimistic message stays
  visible but flips to `Failed` so the user sees what happened.
  Routing-ack subscriber still flips `Pending → Ack` keyed by id —
  unchanged.

Tests added (suite 74 → 76):
- "appends the optimistic message before the send promise resolves"
  — asserts the bucket contains the message synchronously after the
  send call returns its pending promise.
- "flips outbound state to Failed when sendText returns Err"
  — exercises the empty-text validation path.

* adding a claude to repo

* handle device reboots

* chore(web): post-rebase fixups — lockfile, dedupe dep, formatting

- Regenerate pnpm-lock.yaml to match the merged package.json set (adds the
  @meshtastic/sdk + transport workspace packages from this PR); the rebase
  had held the lockfile at main's version via a merge driver.
- Drop a duplicate better-result entry in apps/web/package.json.
- Reformat LoRa.tsx / Display.tsx (oxfmt) after merging main's config fields
  into Dan's ConfigEditor field groups.

* test: retire main's nodeDBStore + messageStore tests superseded by SDK migration

Dan's SDK migration removed the Zustand nodeDBStore (node state now lives in
the SDK; useNodesLegacy notes the migration) and the useNodeDB hook. main had
since added nodeDBStore.test.tsx / messageStore.test.ts and kept evolving the
store; the rebase carried those over, so they tested an API this PR removes
(useNodeDB is not a function). Dan's branch contains none of these files.

Remove the dead nodeDBStore store dir and the two orphaned main tests to match
the migrated architecture. The live messageStore (barrel-exported) is kept; only
main's standalone test for it is dropped.

---------

Co-authored-by: Ben Meadors <benmmeadors@gmail.com>
2026-06-15 16:55:49 -05:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
ccc02ad438 chore(deps): bump @radix-ui/react-dialog from 1.1.15 to 1.1.16 (#1115)
Bumps [@radix-ui/react-dialog](https://github.com/radix-ui/primitives/tree/HEAD/packages/react/dialog) from 1.1.15 to 1.1.16.
- [Changelog](https://github.com/radix-ui/primitives/blob/main/packages/react/dialog/CHANGELOG.md)
- [Commits](https://github.com/radix-ui/primitives/commits/HEAD/packages/react/dialog)

---
updated-dependencies:
- dependency-name: "@radix-ui/react-dialog"
  dependency-version: 1.1.16
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 17:50:07 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
d901ec40ae chore(deps): bump lucide-react from 1.17.0 to 1.18.0 (#1114)
Bumps [lucide-react](https://github.com/lucide-icons/lucide/tree/HEAD/packages/lucide-react) from 1.17.0 to 1.18.0.
- [Release notes](https://github.com/lucide-icons/lucide/releases)
- [Commits](https://github.com/lucide-icons/lucide/commits/1.18.0/packages/lucide-react)

---
updated-dependencies:
- dependency-name: lucide-react
  dependency-version: 1.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 17:47:35 -04:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1fce10a1df chore(deps-dev): bump oxlint from 1.52.0 to 1.69.0 (#1096)
Bumps [oxlint](https://github.com/oxc-project/oxc/tree/HEAD/npm/oxlint) from 1.52.0 to 1.69.0.
- [Release notes](https://github.com/oxc-project/oxc/releases)
- [Changelog](https://github.com/oxc-project/oxc/blob/main/npm/oxlint/CHANGELOG.md)
- [Commits](https://github.com/oxc-project/oxc/commits/oxlint_v1.69.0/npm/oxlint)

---
updated-dependencies:
- dependency-name: oxlint
  dependency-version: 1.69.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-06-15 17:40:43 -04:00
AustinandGitHub 341f3be6d6 fix(lint): remove rule for import/no-unused-modules from .oxlintrc.json (#1111)
import/no-unused-modules seems to have been removed in more recent oxlint versions. Remove it here to unblock oxlint upgrades.
2026-06-15 17:33:06 -04:00
578 changed files with 24198 additions and 14456 deletions
+10 -11
View File
@@ -1,13 +1,12 @@
{ {
"name": "meshtastic-web", "name": "meshtastic-web",
"image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm", "image": "mcr.microsoft.com/devcontainers/typescript-node:1-22-bookworm",
"features": { "features": {
"ghcr.io/r3dpoint/devcontainer-features/tailwindcss-standalone-cli:1": { "ghcr.io/r3dpoint/devcontainer-features/tailwindcss-standalone-cli:1": {
"version": "latest" "version": "latest"
}, },
"ghcr.io/devcontainers-extra/features/pnpm:2": { "ghcr.io/devcontainers-extra/features/pnpm:2": {
"version": "latest" "version": "latest"
} }
} }
} }
+6 -2
View File
@@ -3,17 +3,21 @@ updates:
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "weekly"
- package-ecosystem: "npm" - package-ecosystem: "npm"
directory: "/" directory: "/"
schedule: schedule:
interval: "daily" interval: "weekly"
cooldown: cooldown:
default-days: 3 # 1 day after pnpm's cutoff, prevent a race default-days: 3 # 1 day after pnpm's cutoff, prevent a race
open-pull-requests-limit: 10 # Increase PR limit
groups: groups:
tanstack: tanstack:
patterns: patterns:
- "@tanstack/*" - "@tanstack/*"
radix-ui:
patterns:
- "@radix-ui/*"
react-ecosystem: react-ecosystem:
patterns: patterns:
- react - react
+2 -2
View File
@@ -14,13 +14,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v6 uses: pnpm/action-setup@v6
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v7
with: with:
node-version: 24 node-version: 24
cache: pnpm cache: pnpm
+2 -2
View File
@@ -11,13 +11,13 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Download translations with Crowdin - name: Download translations with Crowdin
uses: crowdin/github-action@v2 uses: crowdin/github-action@v2
with: with:
base_url: "https://meshtastic.crowdin.com/api/v2" base_url: "https://meshtastic.crowdin.com/api/v2"
config: "./packages/web/crowdin.yml" config: "./apps/web/crowdin.yml"
upload_sources: false upload_sources: false
upload_translations: false upload_translations: false
download_translations: true download_translations: true
+3 -3
View File
@@ -5,7 +5,7 @@ on:
# Monitor all .json files within the /i18n/locales/en/ directory. # Monitor all .json files within the /i18n/locales/en/ directory.
# This ensures the workflow triggers if any the English namespace files are modified on the main branch. # This ensures the workflow triggers if any the English namespace files are modified on the main branch.
paths: paths:
- "/packages/web/public/i18n/locales/en/*.json" - "/apps/web/public/i18n/locales/en/*.json"
branches: [main] branches: [main]
workflow_dispatch: # Allow manual triggering workflow_dispatch: # Allow manual triggering
@@ -15,13 +15,13 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Upload sources with Crowdin - name: Upload sources with Crowdin
uses: crowdin/github-action@v2 uses: crowdin/github-action@v2
with: with:
base_url: "https://meshtastic.crowdin.com/api/v2" base_url: "https://meshtastic.crowdin.com/api/v2"
config: "./packages/web/crowdin.yml" config: "./apps/web/crowdin.yml"
upload_sources: true upload_sources: true
upload_translations: false upload_translations: false
download_translations: false download_translations: false
@@ -9,13 +9,13 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Upload translations with Crowdin - name: Upload translations with Crowdin
uses: crowdin/github-action@v2 uses: crowdin/github-action@v2
with: with:
base_url: "https://meshtastic.crowdin.com/api/v2" base_url: "https://meshtastic.crowdin.com/api/v2"
config: "./packages/web/crowdin.yml" config: "./apps/web/crowdin.yml"
upload_sources: false upload_sources: false
upload_translations: true upload_translations: true
download_translations: false download_translations: false
+64
View File
@@ -0,0 +1,64 @@
# Real-device end-to-end suite: drives the web app in Chromium against two
# simulated `meshtasticd` firmware nodes (UDP-multicast mesh, no MQTT) and
# exercises text messaging in both directions. See e2e/README.md.
name: E2E (real device)
on:
workflow_dispatch:
pull_request:
paths:
- "e2e/**"
- "apps/web/**"
- "packages/**"
- "playwright.config.ts"
- ".github/workflows/e2e.yml"
jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 30
env:
CI: "true"
steps:
- uses: actions/checkout@v7
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
- name: Install workspace deps
run: pnpm install --frozen-lockfile
- name: Install Chromium
run: pnpm exec playwright install --with-deps chromium
- uses: actions/setup-python@v7
with:
python-version: "3.11"
- name: Install Python mesh peer
run: |
python -m venv e2e/peer/.venv
e2e/peer/.venv/bin/pip install -r e2e/peer/requirements.txt
- name: Run E2E suite
run: pnpm test:e2e
- name: Upload Playwright report
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v7
with:
name: playwright-report
path: e2e/.report
retention-days: 7
- name: Dump device logs on failure
if: failure()
run: docker compose -f e2e/device/docker-compose.yml logs --no-color || true
- name: Tear down mesh
if: always()
run: docker compose -f e2e/device/docker-compose.yml down -v || true
+1 -1
View File
@@ -10,7 +10,7 @@ jobs:
issues: write issues: write
pull-requests: write pull-requests: write
steps: steps:
- uses: actions/stale@v10 - uses: actions/stale@v11
with: with:
repo-token: ${{ secrets.GITHUB_TOKEN }} repo-token: ${{ secrets.GITHUB_TOKEN }}
days-before-issue-stale: 60 days-before-issue-stale: 60
+4 -4
View File
@@ -18,13 +18,13 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v6 uses: pnpm/action-setup@v6
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v7
with: with:
node-version: 24 node-version: 24
cache: pnpm cache: pnpm
@@ -86,7 +86,7 @@ jobs:
- name: Build Container Image (multi-arch) - name: Build Container Image (multi-arch)
id: build-container id: build-container
uses: redhat-actions/buildah-build@v2 uses: redhat-actions/buildah-build@v3
with: with:
containerfiles: | containerfiles: |
./apps/web/infra/Containerfile ./apps/web/infra/Containerfile
@@ -103,7 +103,7 @@ jobs:
- name: Push To GHCR - 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@v3
with: with:
image: ${{ steps.build-container.outputs.image }} image: ${{ steps.build-container.outputs.image }}
# Push the same tags used at build time: # Push the same tags used at build time:
+3 -2
View File
@@ -21,13 +21,13 @@ jobs:
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v6 uses: pnpm/action-setup@v6
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v7
with: with:
node-version: 24 node-version: 24
cache: pnpm cache: pnpm
@@ -52,6 +52,7 @@ jobs:
- name: Check formatter - name: Check formatter
run: pnpm run check run: pnpm run check
continue-on-error: true
- name: Run tests - name: Run tests
run: pnpm run test run: pnpm run test
+3 -3
View File
@@ -22,13 +22,13 @@ jobs:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v7
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v6 uses: pnpm/action-setup@v6
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v7
with: with:
node-version: 24 node-version: 24
cache: pnpm cache: pnpm
@@ -44,7 +44,7 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
if [ "${{ github.event.inputs.packages }}" = "all" ] || [ -z "${{ github.event.inputs.packages }}" ]; then if [ "${{ github.event.inputs.packages }}" = "all" ] || [ -z "${{ github.event.inputs.packages }}" ]; then
mapfile -t TARGETS < <(ls -d packages/* | grep -v 'packages/web') mapfile -t TARGETS < <(ls -d packages/*)
else else
IFS=',' read -ra TARGETS <<< "${{ github.event.inputs.packages }}" IFS=',' read -ra TARGETS <<< "${{ github.event.inputs.packages }}"
TARGETS=("${TARGETS[@]/#/packages/}") TARGETS=("${TARGETS[@]/#/packages/}")
+3 -3
View File
@@ -16,14 +16,14 @@ on:
permissions: write-all permissions: write-all
env: env:
PROTOBUF_DIR: ./packages/protobufs PROTOBUF_DIR: ./packages/protobufs
jobs: jobs:
codegen: codegen:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
@@ -70,7 +70,7 @@ jobs:
- name: Setup pnpm - name: Setup pnpm
uses: pnpm/action-setup@v6 uses: pnpm/action-setup@v6
- name: Install workspace dependencies - name: Install workspace dependencies
run: pnpm install --frozen-lockfile run: pnpm install --frozen-lockfile
+3 -3
View File
@@ -31,7 +31,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v6 uses: actions/checkout@v7
with: with:
ref: ${{ inputs.ref != '' && inputs.ref || github.ref }} ref: ${{ inputs.ref != '' && inputs.ref || github.ref }}
@@ -39,7 +39,7 @@ jobs:
uses: pnpm/action-setup@v6 uses: pnpm/action-setup@v6
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v6 uses: actions/setup-node@v7
with: with:
node-version: 24 node-version: 24
cache: pnpm cache: pnpm
@@ -119,7 +119,7 @@ jobs:
uses: docker/setup-buildx-action@v4 uses: docker/setup-buildx-action@v4
- name: Login to GHCR - name: Login to GHCR
uses: docker/login-action@v4 uses: docker/login-action@v4.5.2
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
@@ -17,10 +17,10 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v7
with: with:
fetch-depth: 0 fetch-depth: 0
fetch-tags: true # IMPORTANT: we need tags to resolve the release commit fetch-tags: true # IMPORTANT: we need tags to resolve the release commit
- name: Configure Git author - name: Configure Git author
run: | run: |
+3
View File
@@ -18,3 +18,6 @@ packages/protobufs/packages/ts/dist
*.pem *.pem
*.crt *.crt
*.key *.key
# Claude Code session locks
.claude/
+7
View File
@@ -0,0 +1,7 @@
{
"$schema": "https://raw.githubusercontent.com/oxc-project/oxc/main/npm/oxfmt/configuration_schema.json",
"useTabs": false,
"tabWidth": 2,
"printWidth": 80,
"ignorePatterns": ["*.md"]
}
+1 -1
View File
@@ -1,3 +1,3 @@
{ {
"recommendations": ["bradlc.vscode-tailwindcss", "biomejs.biome"] "recommendations": ["bradlc.vscode-tailwindcss", "oxc.oxc-vscode"]
} }
+2 -2
View File
@@ -2,5 +2,5 @@
"editor.formatOnSave": true, "editor.formatOnSave": true,
"search.exclude": { "search.exclude": {
"**/i18n/locales/*-*/**": true "**/i18n/locales/*-*/**": true
}, }
} }
+9
View File
@@ -0,0 +1,9 @@
# Meshtastic Web Client - Claude Code Guide
@AGENTS.md
## Claude-Specific Instructions
- **Think First:** Always outline your step-by-step reasoning inside `<thinking>` tags before writing code or shell commands. Claude models perform significantly better on complex TypeScript tasks when they "think out loud" first.
- **Skills:** The `.skills/` directory contains task-specific instruction modules. Load them as needed — only the skill relevant to your current task.
- **Plan Mode:** Use plan mode for architectural changes spanning multiple modules. Write plans to `.agent_plans/` (git-ignored). The Copilot-CLI-specific `/plan`, `/delegate`, `/research`, and `/share` guidance in `AGENTS.md` does not apply to Claude Code — skip the `<copilot_cli_workflow>` section.
+143 -65
View File
@@ -9,32 +9,151 @@
## Overview ## Overview
This monorepo consolidates the official [Meshtastic](https://meshtastic.org) web This monorepo consolidates the official [Meshtastic](https://meshtastic.org) web
interface and its supporting JavaScript libraries. It aims to provide a unified interface, the domain-driven JavaScript SDK that drives it, and a set of
development experience for interacting with Meshtastic devices. runtime-specific transport packages. Everything you need to read state from or
send commands to a Meshtastic device lives here.
> [!NOTE] > [!NOTE]
> You can find the main Meshtastic documentation at https://meshtastic.org/docs/introduction/. > You can find the main Meshtastic documentation at https://meshtastic.org/docs/introduction/.
### Projects within this Monorepo (`packages/`) ## Packages
All projects are located within the `packages/` directory: All projects live under `packages/`.
- **`packages/web` (Meshtastic Web Client):** The official web interface, | Package | Purpose |
designed to be hosted or served directly from a Meshtastic node. | --- | --- |
- **[Hosted version](https://client.meshtastic.org)** | `packages/sdk` | Framework-agnostic TypeScript SDK. Domain-driven feature slices (device, chat, nodes, channels, config, telemetry, position, traceroute, files) built around a `MeshClient` orchestrator with `@preact/signals-core` reactive state. |
- **`packages/core`:** Core functionality for Meshtastic JS. | `packages/sdk-react` | React hooks + `MeshProvider` on top of `@meshtastic/sdk`. Wraps signals in `useSyncExternalStore` for concurrent-safe renders. |
- **`packages/transport-node`:** TCP Transport for the NodeJS runtime. | `apps/web` | Reference React web client. Hosted at [client.meshtastic.org](https://client.meshtastic.org). |
- **`packages/transport-node-serial`:** NodeJS Serial Transport for the NodeJS runtime. | `packages/ui` | Shared Radix + Tailwind component library. |
- **`packages/transport-deno`:** TCP Transport for the Deno runtime. | `packages/protobufs` | Generated TypeScript stubs from [`meshtastic/protobufs`](https://github.com/meshtastic/protobufs), produced via `buf generate`. Source of truth for every wire-level type. |
- **`packages/transport-http`:** HTTP Transport. | `packages/transport-http` | HTTP transport for devices exposing a network interface. |
- **`packages/transport-web-bluetooth`:** Web Bluetooth Transport. | `packages/transport-web-bluetooth` | Web Bluetooth transport for BLE-capable devices (browsers). |
- **`packages/transport-web-serial`:** Web Serial Transport. | `packages/transport-web-serial` | Web Serial transport for USB-serial devices (browsers). |
- **`packages/protobufs`:** Git submodule containing Meshtastic’s shared protobuf definitions, used to generate and publish the JSR protobuf package. | `packages/transport-node` | TCP transport for Node.js. |
| `packages/transport-node-serial` | Serial transport for Node.js. |
| `packages/transport-deno` | TCP transport for Deno. |
| `packages/transport-mock` | In-memory transport for tests. |
All `Meshtastic JS` packages (core and transports) are published both to All publishable packages ship to both [JSR](https://jsr.io/@meshtastic) and [NPM](https://www.npmjs.com/org/meshtastic).
[JSR](https://jsr.io/@meshtastic). [NPM](https://www.npmjs.com/org/meshtastic)
--- ## Architecture
`@meshtastic/sdk` organises its source by feature slice. Each slice follows the
same DDD layout:
```
features/<slice>/
domain/ # entities & value objects — pure TypeScript types
application/ # use-cases (SendTextUseCase, FavoriteNodeUseCase, …)
infrastructure/ # protobuf ↔ domain mappers, admin-message adapters
state/ # signals-backed reactive stores
<Slice>Client.ts # public facade exposing readable signals + command methods
index.ts
```
The shared kernel under `packages/sdk/src/core/` owns:
- **`client/`** — `MeshClient`, the thin orchestrator that owns the transport, queue, event bus, and one instance of every slice client.
- **`transport/`** — the `Transport` interface every `transport-*` package implements.
- **`event-bus/`** — typed pub/sub channels populated by the packet codec.
- **`packet-codec/`** — frame parser (0x94 0xC3), `FromRadio` decoder, portnum router.
- **`queue/`**, **`xmodem/`** — packet ack/timeout pipeline and file-transfer protocol.
- **`signals/`** — signal and keyed-collection helpers consumed by every slice.
- **`logging/`** — `tslog` factory used consistently by every class.
- **`identifiers/`**, **`errors/`** — small primitives shared across slices.
The protobuf boundary is strict: wire messages enter through the packet codec,
get mapped into domain entities inside `features/*/infrastructure/*Mapper.ts`,
and signals only ever expose the domain shape.
```
Transport ─▶ Packet codec ─▶ EventBus ─▶ Slice infrastructure
│
▼
Signals (state) ─▶ sdk-react hooks ─▶ UI
▲
│
Slice application (use-cases) ─▶ MeshClient.sendPacket ─▶ Queue ─▶ Transport
```
Expected domain errors are returned as `Result<T, E>` via [`better-result`](https://www.npmjs.com/package/better-result); exceptions are reserved for programmer errors and truly exceptional conditions.
## Getting Started
### Prerequisites
You need [pnpm](https://pnpm.io/) installed. If you plan to regenerate
protobufs, also install the [Buf CLI](https://buf.build/docs/cli/installation/).
### Setup
```bash
git clone https://github.com/meshtastic/web.git
cd web
pnpm install
```
### Run the web client
```bash
pnpm --filter @meshtastic/web dev
```
### Build everything
```bash
pnpm -r build
```
### Run tests
```bash
pnpm -r test
```
### Lint + format
```bash
pnpm check
pnpm check:fix
```
## Developing
### Adding a new feature slice to `@meshtastic/sdk`
1. Create `packages/sdk/src/features/<slice>/` with `domain/`, `application/`, `infrastructure/`, `state/` subdirectories plus an `index.ts` barrel.
2. Implement a signals-backed store in `state/`.
3. Subscribe to the relevant `EventBus` channel(s) inside a `<Slice>Client.ts` class and write mapped domain entities to the store.
4. Wire the new client into `MeshClient` and re-export types from `packages/sdk/mod.ts`.
5. Add vitest coverage: domain invariants, use-cases against `createFakeTransport()`, and round-trip mapper fixtures.
6. If the slice has React callers, add a matching hook under `packages/sdk-react/src/hooks/`.
### Adding a new transport
Implement the `Transport` interface exported from `@meshtastic/sdk/transport`:
```ts
interface Transport {
toDevice: WritableStream<Uint8Array>;
fromDevice: ReadableStream<DeviceOutput>;
disconnect(): Promise<void>;
}
```
The SDK does the framing and decoding — transports only supply raw bytes.
### Testing
Vitest is wired at the repo root and picks up `packages/*` projects. The SDK
ships `@meshtastic/sdk/testing` with `createFakeTransport()` for wiring tests
without real hardware.
## Publishing
Each publishable package has `build:npm` / `publish:npm` / `prepare:jsr` /
`publish:jsr` scripts. See each package's `package.json` for details.
## Repository activity ## Repository activity
@@ -42,56 +161,11 @@ All `Meshtastic JS` packages (core and transports) are published both to
| :------------- | :-------------------------------------------------------------------------------------------------------------------- | | :------------- | :-------------------------------------------------------------------------------------------------------------------- |
| Meshtastic Web | ![Alt](https://repobeats.axiom.co/api/embed/e5b062db986cb005d83e81724c00cb2b9cce8e4c.svg "Repobeats analytics image") | | Meshtastic Web | ![Alt](https://repobeats.axiom.co/api/embed/e5b062db986cb005d83e81724c00cb2b9cce8e4c.svg "Repobeats analytics image") |
--- ## Feedback
## Tech Stack
This monorepo leverages the following technologies:
- **Runtime:** pnpm / Deno
- **Web Client:** React.js
- **Styling:** Tailwind CSS
- **Bundling:** Vite
- **Language:** TypeScript
- **Testing:** Vitest, React Testing Library
---
## Getting Started
### Prerequisites
You'll need to have [pnpm](https://pnpm.io/) installed to work with this monorepo.
Follow the installation instructions on their home page.
### Development Setup
1. **Clone the repository:**
```bash
git clone https://github.com/meshtastic/meshtastic-web.git
cd meshtastic-web
```
2. **Install dependencies for all packages:**
```bash
pnpm install
```
This command installs all necessary dependencies for all packages within the
monorepo.
3. **Install the Buf CLI**
Required for building `packages/protobufs`
https://buf.build/docs/cli/installation/
### Running Projects
#### Meshtastic Web Client
Please refer to the [Meshtastic Web README](packages/web/README.md) for setup and usage.
### Feedback
If you encounter any issues, please report them in our If you encounter any issues, please report them in our
[issues tracker](https://github.com/meshtastic/web/issues). Your feedback helps [issues tracker](https://github.com/meshtastic/web/issues). Your feedback helps
improve the stability of future releases improve the stability of future releases.
## Star history ## Star history
@@ -108,3 +182,7 @@ improve the stability of future releases
<a href="https://github.com/meshtastic/web/graphs/contributors"> <a href="https://github.com/meshtastic/web/graphs/contributors">
<img src="https://contrib.rocks/image?repo=meshtastic/web" width="100%"/> <img src="https://contrib.rocks/image?repo=meshtastic/web" width="100%"/>
</a> </a>
## License
GPL-3.0-only. See [LICENSE](LICENSE).
+83
View File
@@ -0,0 +1,83 @@
# Testing strategy
How this monorepo proves correctness, and where coverage currently sits.
## Levels
Tests live in five tiers. Each PR should add coverage at the **lowest level that catches the regression**, and only climb tiers when that's not possible.
| Tier | Scope | Tooling | Where |
| --- | --- | --- | --- |
| 1. **Unit** | Pure functions, value objects, mappers, single classes with mocked deps | `vitest run` (Node env) | `*.test.ts` colocated with source |
| 2. **Slice integration** | Use-case + store + mapper exercised against in-memory deps; assert outbound bytes and signal state | `vitest` + `createFakeTransport()` | `*.test.ts` in slice dirs |
| 3. **Client integration** | `MeshClient` end-to-end with a fake transport feeding canned `FromRadio` packets | `vitest` + `@meshtastic/sdk/testing` | `packages/sdk/tests/integration/` |
| 4. **Storage integration** | Real Drizzle queries against sql.js in-memory; or against `@vitest/browser` for OPFS | `vitest` (Node + browser) | `packages/sdk-storage-sqlocal/{src,tests}` |
| 5. **Hook / DOM** | React hooks render under `<MeshProvider>` / `<MeshRegistryProvider>`, react to signals | `vitest` + `@testing-library/react` + `jsdom` | `packages/sdk-react/tests/` |
| 6. **E2E / simulator** | Whole stack: SDK → transport → simulator/firmware. Catches protocol drift | `@vitest/browser` for OPFS; `meshtasticd` simulator over TCP for protocol | future `tests/e2e/` |
## Per-package coverage gates
| Package | Required floor | Notes |
| --- | --- | --- |
| `packages/sdk/core` | every primitive (signals, EventBus, Queue, packet-codec, identifiers) has a unit test; lifecycle covered by `MeshClient.test.ts` | adopt `c8` thresholds once stable |
| `packages/sdk/features/*` | each slice ships: 1 domain invariant test, 1 use-case test against fake transport, 1 mapper round-trip (where mappers exist) | Integration covered by `tests/integration/fake-transport.test.ts` |
| `packages/sdk-react` | each public hook has a `renderHook` test that asserts initial render + re-render on signal change | uses jsdom; provider wrapper required |
| `packages/sdk-storage-sqlocal` | every repository method tested against sql.js in-memory; **at least one OPFS-real test per repo** runs in browser mode | sql.js validates SQL correctness; OPFS validates VFS / Worker plumbing |
| `packages/transport-*` | minimum: framing round-trip, disconnect cleans up streams, error path emits status | low-level; ship as is |
| `packages/web` | component tests at `34` baseline; new SDK-driven UI components must add a hook-mock test | currently all green |
## Current state (audit)
| Package | Test files | Tests pass | Gaps |
| --- | --- | --- | --- |
| `packages/sdk` | 7 | 25 ✅ | nodes/channels/config/telemetry/position/traceroute/files slices have **no tests**; no `MeshClient` lifecycle test (only fake-transport integration); no schema migration test |
| `packages/sdk-react` | 1 | 2 ✅ | only `useMeshDevice` + a stubbed `useChat` test; missing `useNodes`, `useChannels`, `useConfig`, `useConnection`, `useTraceroute`, `useTelemetry`, `usePosition`, `useFileTransfer`, `useFavoriteNode`, `useIgnoreNode`, `useMeshRegistry`, `useClientById`, `useActiveClient`. No registry-aware re-render coverage. |
| `packages/sdk-storage-sqlocal` | 2 | 8 ✅ | sql.js only — **no real OPFS test**, no Worker boot test, no cross-tab BroadcastChannel test (mocked away), no migration v1→v2 test |
| `packages/web` | 34 | 294 ✅ | no `useConnections` test; new `meshRegistry` + `sdkStorage` modules untested; chat persistence end-to-end not exercised |
| `packages/transport-*` | 1 each (5 of 7) | varies | `transport-deno` + `transport-mock` have no tests; transports likely lack disconnect/error coverage |
| `packages/core` | 0 | n/a | legacy, slated for deletion in Phase C; tolerable |
| `packages/ui` | 0 | n/a | pure presentational; visual regression only |
| `packages/protobufs` | 0 | n/a | generated code; upstream's responsibility |
## Concrete additions queued (priority order)
1. **`packages/sdk` slice tests** — one Use-case + one Mapper test per slice (`nodes`, `channels`, `config`, `telemetry`, `position`, `traceroute`). Pattern: build a stub `MeshClient`, dispatch a synthetic event, assert signal value or outbound bytes.
2. **`packages/sdk-react` hook tests** — for every hook listed above, mount under `<MeshProvider>`, drive a signal change, assert `result.current`. One file, ~15 cases.
3. **`packages/sdk` `ChatClient` persistence test** — wire `InMemoryMessageRepository`, append messages, re-construct `ChatClient`, assert hydration. Validates the lazy-load contract.
4. **`packages/sdk-storage-sqlocal` migration test** — bootstrap empty DB, run `MIGRATIONS[]`, assert `_schema.version`. Then add a fake `version: 2` migration and prove it's applied idempotently.
5. **`packages/sdk-storage-sqlocal` browser mode** — add a second `vitest.browser.config.ts` using `@vitest/browser` (Playwright provider) so we exercise real OPFS + Worker. CI runs both modes.
6. **`packages/sdk-storage-sqlocal` BroadcastChannel test** — instantiate two `MultiTabCoordinator` instances in same process; one broadcasts, the other observes. (Node has no `BroadcastChannel` global; use `worker_threads`'s `BroadcastChannel` polyfill or jsdom env.)
7. **`packages/web` `meshRegistry` + `sdkStorage` lazy-init test** — assert `getStorageDb()` returns the same promise on repeated calls and only opens the DB once.
8. **`packages/sdk-react` registry test** — mount `<MeshRegistryProvider>` with two clients, switch active, confirm a hook re-renders against the new client.
## E2E / simulator (Tier 6) — scope only
Out of immediate scope; documenting for a follow-up PR.
- Run `meshtasticd` (firmware simulator) in CI Docker via `services:` block.
- Spin up `MeshClient` with `TransportHTTP` pointed at the simulator's HTTP endpoint.
- Drive scripted scenarios: configure → send text → expect ack; channel update; node info exchange; traceroute.
- Use `@vitest/browser` so we also exercise the real OPFS persistence path during E2E.
- Run on `main` only (cost). Smoke subset on PR.
Until that lands, `createFakeTransport()` covers the protocol layer at unit/integration speed.
## Conventions
- Test file colocated with source: `Foo.ts` → `Foo.test.ts`.
- Integration tests under `tests/integration/`.
- Browser-mode tests use the suffix `.browser.test.ts` so they can be filtered.
- Protobuf fixtures live in `__fixtures__/*.fixtures.ts` next to mappers; binary data committed as base64, not raw bytes.
- Each test imports concrete classes from the source path (`./Foo.ts`), not the package barrel — fast type-check, zero re-export drift.
- No mocked SDK from inside SDK tests. Use `createFakeTransport()` and real `MeshClient` instances.
## Running
```sh
pnpm -r test # all packages, Node env
pnpm --filter @meshtastic/sdk test
pnpm --filter @meshtastic/sdk-storage-sqlocal test
pnpm --filter meshtastic-web test
# future:
pnpm --filter @meshtastic/sdk-storage-sqlocal test:browser
```
+1
View File
@@ -0,0 +1 @@
@jsr:registry=https://npm.jsr.io
+2 -1
View File
@@ -8,6 +8,7 @@
"dist", "dist",
"protobufs", "protobufs",
"**/locales/*-*/*.json", "**/locales/*-*/*.json",
"**/packages/ui/" "**/packages/ui/",
"*.md"
] ]
} }
+1 -2
View File
@@ -4,8 +4,7 @@
"rules": { "rules": {
"no-debugger": "error", "no-debugger": "error",
"@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unused-vars": "error", "@typescript-eslint/no-unused-vars": "error"
"import/no-unused-modules": "error"
}, },
"ignorePatterns": [ "ignorePatterns": [
"node_modules", "node_modules",
+2 -6
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html> <!doctype html>
<html lang="en" data-theme="system"> <html lang="en" data-theme="system">
<head> <head>
<!-- CookieYes banner --> <!-- CookieYes banner -->
@@ -11,11 +11,7 @@
<link rel="manifest" href="site.webmanifest" /> <link rel="manifest" href="site.webmanifest" />
<link rel="mask-icon" href="logo_black.svg" color="#67ea94" /> <link rel="mask-icon" href="logo_black.svg" color="#67ea94" />
<link <link rel="stylesheet" href="src/index.css" crossorigin="anonymous" />
rel="stylesheet"
href="src/index.css"
crossorigin="anonymous"
/>
<meta name="theme-color" content="#67ea94" /> <meta name="theme-color" content="#67ea94" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="description" content="Meshtastic Web Client" /> <meta name="description" content="Meshtastic Web Client" />
+72 -70
View File
@@ -1,17 +1,17 @@
{ {
"name": "meshtastic-web", "name": "meshtastic-web",
"version": "2.6.6-0", "version": "2.6.6-0",
"type": "module",
"description": "Meshtastic web client", "description": "Meshtastic web client",
"homepage": "https://meshtastic.org",
"bugs": {
"url": "https://github.com/meshtastic/web/issues"
},
"license": "GPL-3.0-only", "license": "GPL-3.0-only",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "git+https://github.com/meshtastic/web.git" "url": "git+https://github.com/meshtastic/web.git"
}, },
"bugs": { "type": "module",
"url": "https://github.com/meshtastic/web/issues"
},
"homepage": "https://meshtastic.org",
"scripts": { "scripts": {
"preinstall": "npx only-allow pnpm", "preinstall": "npx only-allow pnpm",
"setup:certs": "mkcert localhost 127.0.0.1 ::1", "setup:certs": "mkcert localhost 127.0.0.1 ::1",
@@ -26,98 +26,100 @@
"dev": "vite", "dev": "vite",
"dev:https": "USE_HTTPS=true vite", "dev:https": "USE_HTTPS=true vite",
"test": "vitest", "test": "vitest",
"typecheck": "pnpm run tsc --noEmit", "typecheck": "tsc -p tsconfig.json --noEmit",
"preview": "vite preview", "preview": "vite preview",
"docker:build": "docker build -t meshtastic-web:latest -f ./infra/Containerfile .", "docker:build": "docker build -t meshtastic-web:latest -f ./infra/Containerfile .",
"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/ ."
}, },
"dependencies": { "dependencies": {
"@hookform/resolvers": "^5.2.2", "@hookform/resolvers": "^5.5.7",
"@meshtastic/core": "workspace:*", "@meshtastic/sdk": "workspace:*",
"@meshtastic/sdk-react": "workspace:*",
"@meshtastic/sdk-storage-sqlocal": "workspace:*",
"@meshtastic/transport-http": "workspace:*", "@meshtastic/transport-http": "workspace:*",
"@meshtastic/transport-web-bluetooth": "workspace:*", "@meshtastic/transport-web-bluetooth": "workspace:*",
"@meshtastic/transport-web-serial": "workspace:*", "@meshtastic/transport-web-serial": "workspace:*",
"@noble/curves": "^1.9.2", "@noble/curves": "^2.2.0",
"@preact/signals-core": "^1.8.0", "@preact/signals-core": "^1.14.4",
"better-result": "^1.0.0", "@radix-ui/react-accordion": "^1.2.20",
"@radix-ui/react-accordion": "^1.2.13", "@radix-ui/react-alert-dialog": "^1.1.23",
"@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-checkbox": "^1.3.11",
"@radix-ui/react-checkbox": "^1.3.3", "@radix-ui/react-dialog": "^1.1.23",
"@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.24",
"@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-label": "^2.1.15",
"@radix-ui/react-label": "^2.1.7", "@radix-ui/react-menubar": "^1.1.24",
"@radix-ui/react-menubar": "^1.1.17", "@radix-ui/react-popover": "^1.1.23",
"@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-scroll-area": "^1.2.18",
"@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-select": "^2.3.7",
"@radix-ui/react-select": "^2.2.6", "@radix-ui/react-separator": "^1.1.15",
"@radix-ui/react-separator": "^1.1.7", "@radix-ui/react-slider": "^1.4.7",
"@radix-ui/react-slider": "^1.3.6", "@radix-ui/react-switch": "^1.3.7",
"@radix-ui/react-switch": "^1.2.6", "@radix-ui/react-tabs": "^1.1.21",
"@radix-ui/react-tabs": "^1.1.13", "@radix-ui/react-toast": "^1.2.23",
"@radix-ui/react-toast": "^1.2.15", "@radix-ui/react-toggle-group": "^1.1.19",
"@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-tooltip": "^1.2.16",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-visually-hidden": "^1.2.11",
"@radix-ui/react-visually-hidden": "^1.2.3", "@tailwindcss/vite": "^4.3.3",
"@tailwindcss/vite": "^4.1.14", "@tanstack/react-router": "^1.170.18",
"@tanstack/react-router": "^1.170.15",
"@tanstack/react-router-devtools": "^1.167.0", "@tanstack/react-router-devtools": "^1.167.0",
"@tanstack/router-cli": "^1.167.17", "@tanstack/router-cli": "^1.167.21",
"@tanstack/router-devtools": "^1.167.0", "@tanstack/router-devtools": "^1.167.0",
"@turf/turf": "^7.2.0", "@turf/turf": "^7.3.5",
"@types/node": "^24.7.0", "@types/node": "^26.1.2",
"@types/web-bluetooth": "^0.0.21", "@types/web-bluetooth": "^0.0.21",
"base64-js": "^1.5.1", "base64-js": "^1.5.1",
"better-result": "^2.10.0",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"crypto-random-string": "^5.0.0", "crypto-random-string": "^6.0.0",
"i18next": "^26.3.1", "i18next": "^26.3.6",
"i18next-browser-languagedetector": "^8.2.0", "i18next-browser-languagedetector": "^8.2.0",
"i18next-http-backend": "^4.0.0", "i18next-http-backend": "^4.0.1",
"idb-keyval": "^6.2.2", "idb-keyval": "^6.3.0",
"immer": "^11.1.8", "immer": "^11.1.15",
"js-cookie": "^3.0.7", "js-cookie": "^3.0.8",
"lucide-react": "^1.17.0", "lucide-react": "^1.28.0",
"maplibre-gl": "5.8.0", "maplibre-gl": "6.3.0",
"react": "^19.2.0", "react": "^19.2.8",
"react-dom": "^19.2.0", "react-dom": "^19.2.8",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.1.2",
"react-hook-form": "^7.78.0", "react-hook-form": "^7.82.0",
"react-i18next": "^17.0.8", "react-i18next": "^17.0.11",
"react-map-gl": "8.1.0", "react-map-gl": "8.1.2",
"react-qrcode-logo": "^4.0.0", "react-qrcode-logo": "^4.1.0",
"rfc4648": "^1.5.4", "rfc4648": "^1.5.4",
"vite": "^7.3.5", "vite": "^8.1.5",
"vite-plugin-html": "^3.2.2", "vite-plugin-html": "^3.2.2",
"vite-plugin-pwa": "^1.3.0", "vite-plugin-pwa": "^1.3.0",
"zod": "^4.1.12", "zod": "^4.1.12",
"zustand": "5.0.8" "zustand": "5.0.14"
}, },
"devDependencies": { "devDependencies": {
"@tanstack/router-plugin": "^1.168.18", "@tanstack/router-plugin": "^1.168.22",
"@testing-library/jest-dom": "^6.9.1", "@testing-library/jest-dom": "^7.0.0",
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.4",
"@types/chrome": "^0.1.22", "@types/chrome": "^0.2.2",
"@types/geojson": "^7946.0.16", "@types/geojson": "^7946.0.16",
"@types/js-cookie": "^3.0.6", "@types/js-cookie": "^3.0.6",
"@types/react": "^19.2.1", "@types/react": "^19.2.17",
"@types/react-dom": "^19.2.0", "@types/react-dom": "^19.2.3",
"@types/serviceworker": "^0.0.158", "@types/serviceworker": "^0.0.200",
"@types/w3c-web-serial": "^1.0.8", "@types/w3c-web-serial": "^1.0.8",
"@vitejs/plugin-basic-ssl": "^2.1.0", "@vitejs/plugin-basic-ssl": "^2.3.0",
"@vitejs/plugin-react": "^5.0.4", "@vitejs/plugin-react": "^6.0.4",
"autoprefixer": "^10.4.21", "autoprefixer": "^10.5.4",
"gzipper": "^8.2.1", "gzipper": "^8.3.0",
"happy-dom": "^20.8.9", "happy-dom": "^20.11.1",
"oxfmt": "^0.16.0", "oxfmt": "^0.63.0",
"oxlint": "^1.41.0", "oxlint": "^1.75.0",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.6.0",
"tailwindcss": "^4.1.14", "tailwindcss": "^4.3.3",
"tailwindcss-animate": "^1.0.7", "tailwindcss-animate": "^1.0.7",
"tar": "^7.5.16", "tar": "^7.5.22",
"testing-library": "^0.0.2", "testing-library": "^0.0.2",
"typescript": "^5.9.3", "typescript": "^7.0.2",
"vitest": "^4.1.8" "vitest": "^4.1.10"
} }
} }
@@ -14,7 +14,7 @@
"tabTelemetry": "Telemetry" "tabTelemetry": "Telemetry"
}, },
"ambientLighting": { "ambientLighting": {
"title": "Ambient Lighting Settings", "title": "Ambient Lighting",
"description": "Settings for the Ambient Lighting module", "description": "Settings for the Ambient Lighting module",
"ledState": { "ledState": {
"label": "LED State", "label": "LED State",
@@ -35,217 +35,253 @@
"blue": { "blue": {
"label": "Blue", "label": "Blue",
"description": "Sets the blue LED level. Values are 0-255" "description": "Sets the blue LED level. Values are 0-255"
},
"ambientLightingConfig": {
"label": "Ambient Lighting Config",
"description": "Ambient Lighting configuration"
} }
}, },
"audio": { "audio": {
"title": "Audio Settings", "title": "Audio",
"description": "Settings for the Audio module", "description": "Settings for the Audio module",
"codec2Enabled": { "codec2Enabled": {
"label": "Codec 2 Enabled", "label": "CODEC 2 enabled",
"description": "Enable Codec 2 audio encoding" "description": "Enable Codec 2 audio encoding"
}, },
"pttPin": { "pttPin": {
"label": "PTT Pin", "label": "PTT pin",
"description": "GPIO pin to use for PTT" "description": "GPIO pin to use for PTT"
}, },
"bitrate": { "bitrate": {
"label": "Bitrate", "label": "CODEC2 sample rate",
"description": "Bitrate to use for audio encoding" "description": "Bitrate to use for audio encoding"
}, },
"i2sWs": { "i2sWs": {
"label": "i2S WS", "label": "I2S word select",
"description": "GPIO pin to use for i2S WS" "description": "GPIO pin to use for i2S WS"
}, },
"i2sSd": { "i2sSd": {
"label": "i2S SD", "label": "I2S data in",
"description": "GPIO pin to use for i2S SD" "description": "GPIO pin to use for i2S SD"
}, },
"i2sDin": { "i2sDin": {
"label": "i2S DIN", "label": "I2S data out",
"description": "GPIO pin to use for i2S DIN" "description": "GPIO pin to use for i2S DIN"
}, },
"i2sSck": { "i2sSck": {
"label": "i2S SCK", "label": "I2S clock",
"description": "GPIO pin to use for i2S SCK" "description": "GPIO pin to use for i2S SCK"
},
"audioConfig": {
"label": "Audio Config",
"description": "Audio configuration"
} }
}, },
"cannedMessage": { "cannedMessage": {
"title": "Canned Message Settings", "title": "Canned Message",
"description": "Settings for the Canned Message module", "description": "Settings for the Canned Message module",
"moduleEnabled": { "moduleEnabled": {
"label": "Module Enabled", "label": "Module Enabled",
"description": "Enable Canned Message" "description": "Enable Canned Message"
}, },
"rotary1Enabled": { "rotary1Enabled": {
"label": "Rotary Encoder #1 Enabled", "label": "Rotary encoder #1 enabled",
"description": "Enable the rotary encoder" "description": "Enable the rotary encoder"
}, },
"inputbrokerPinA": { "inputbrokerPinA": {
"label": "Encoder Pin A", "label": "GPIO pin for rotary encoder A port",
"description": "GPIO Pin Value (1-39) For encoder port A" "description": "GPIO Pin Value (1-39) For encoder port A"
}, },
"inputbrokerPinB": { "inputbrokerPinB": {
"label": "Encoder Pin B", "label": "GPIO pin for rotary encoder B port",
"description": "GPIO Pin Value (1-39) For encoder port B" "description": "GPIO Pin Value (1-39) For encoder port B"
}, },
"inputbrokerPinPress": { "inputbrokerPinPress": {
"label": "Encoder Pin Press", "label": "GPIO pin for rotary encoder Press port",
"description": "GPIO Pin Value (1-39) For encoder Press" "description": "GPIO Pin Value (1-39) For encoder Press"
}, },
"inputbrokerEventCw": { "inputbrokerEventCw": {
"label": "Clockwise event", "label": "Generate input event on CW",
"description": "Select input event." "description": "Select input event."
}, },
"inputbrokerEventCcw": { "inputbrokerEventCcw": {
"label": "Counter Clockwise event", "label": "Generate input event on CCW",
"description": "Select input event." "description": "Select input event."
}, },
"inputbrokerEventPress": { "inputbrokerEventPress": {
"label": "Press event", "label": "Generate input event on Press",
"description": "Select input event" "description": "Select input event"
}, },
"updown1Enabled": { "updown1Enabled": {
"label": "Up Down enabled", "label": "Up/Down/Select input enabled",
"description": "Enable the up / down encoder" "description": "Enable the up / down encoder"
}, },
"allowInputSource": { "allowInputSource": {
"label": "Allow Input Source", "label": "Allow input source",
"description": "Select from: '_any', 'rotEnc1', 'upDownEnc1', 'cardkb'" "description": "Select from: '_any', 'rotEnc1', 'upDownEnc1', 'cardkb'"
}, },
"sendBell": { "sendBell": {
"label": "Send Bell", "label": "Send bell",
"description": "Sends a bell character with each message" "description": "Sends a bell character with each message"
},
"cannedMessageConfig": {
"label": "Canned Message Config",
"description": "Canned Message configuration"
},
"enabled": {
"label": "Canned message enabled",
"description": "Enable Canned Message"
} }
}, },
"detectionSensor": { "detectionSensor": {
"title": "Detection Sensor Settings", "title": "Detection Sensor",
"description": "Settings for the Detection Sensor module", "description": "Settings for the Detection Sensor module",
"enabled": { "enabled": {
"label": "Enabled", "label": "Detection Sensor enabled",
"description": "Enable or disable Detection Sensor Module" "description": "Enable or disable Detection Sensor Module"
}, },
"minimumBroadcastSecs": { "minimumBroadcastSecs": {
"label": "Minimum Broadcast Seconds", "label": "Minimum broadcast (seconds)",
"description": "The interval in seconds of how often we can send a message to the mesh when a state change is detected" "description": "Minimum interval between detection broadcasts"
}, },
"stateBroadcastSecs": { "stateBroadcastSecs": {
"label": "State Broadcast Seconds", "label": "State broadcast (seconds)",
"description": "The interval in seconds of how often we should send a message to the mesh with the current state regardless of changes" "description": "How often to broadcast detection state"
}, },
"sendBell": { "sendBell": {
"label": "Send Bell", "label": "Send bell with alert message",
"description": "Send ASCII bell with alert message" "description": "Send a bell character with the alert"
}, },
"name": { "name": {
"label": "Friendly Name", "label": "Friendly name",
"description": "Used to format the message sent to mesh, max 20 Characters" "description": "Friendly name for the detected event"
}, },
"monitorPin": { "monitorPin": {
"label": "Monitor Pin", "label": "GPIO pin to monitor",
"description": "The GPIO pin to monitor for state changes" "description": "GPIO pin to monitor for detection"
}, },
"detectionTriggerType": { "detectionTriggerType": {
"label": "Detection Triggered Type", "label": "Detection trigger type",
"description": "The type of trigger event to be used" "description": "Trigger type for detection"
}, },
"usePullup": { "usePullup": {
"label": "Use Pullup", "label": "Use INPUT_PULLUP mode",
"description": "Whether or not use INPUT_PULLUP mode for GPIO pin" "description": "Enable internal pull-up on the monitor pin"
},
"detectionSensorConfig": {
"label": "Detection Sensor Config",
"description": "Detection Sensor configuration"
} }
}, },
"externalNotification": { "externalNotification": {
"title": "External Notification Settings", "title": "External Notification",
"description": "Configure the external notification module", "description": "Configure the external notification module",
"enabled": { "enabled": {
"label": "Module Enabled", "label": "External notification enabled",
"description": "Enable External Notification" "description": "Enable External Notification"
}, },
"outputMs": { "outputMs": {
"label": "Output MS", "label": "Output duration (milliseconds)",
"description": "Output MS" "description": "Output MS"
}, },
"output": { "output": {
"label": "Output", "label": "Output LED (GPIO)",
"description": "Output" "description": "Output"
}, },
"outputVibra": { "outputVibra": {
"label": "Output Vibrate", "label": "Output vibra (GPIO)",
"description": "Output Vibrate" "description": "Output Vibrate"
}, },
"outputBuzzer": { "outputBuzzer": {
"label": "Output Buzzer", "label": "Output buzzer (GPIO)",
"description": "Output Buzzer" "description": "Output Buzzer"
}, },
"active": { "active": {
"label": "Active", "label": "Output LED active high",
"description": "Active" "description": "Active"
}, },
"alertMessage": { "alertMessage": {
"label": "Alert Message", "label": "Alert message LED",
"description": "Alert Message" "description": "Alert Message"
}, },
"alertMessageVibra": { "alertMessageVibra": {
"label": "Alert Message Vibrate", "label": "Alert message vibra",
"description": "Alert Message Vibrate" "description": "Alert Message Vibrate"
}, },
"alertMessageBuzzer": { "alertMessageBuzzer": {
"label": "Alert Message Buzzer", "label": "Alert message buzzer",
"description": "Alert Message Buzzer" "description": "Alert Message Buzzer"
}, },
"alertBell": { "alertBell": {
"label": "Alert Bell", "label": "Alert bell LED",
"description": "Should an alert be triggered when receiving an incoming bell?" "description": "Should an alert be triggered when receiving an incoming bell?"
}, },
"alertBellVibra": { "alertBellVibra": {
"label": "Alert Bell Vibrate", "label": "Alert bell vibra",
"description": "Alert Bell Vibrate" "description": "Alert Bell Vibrate"
}, },
"alertBellBuzzer": { "alertBellBuzzer": {
"label": "Alert Bell Buzzer", "label": "Alert bell buzzer",
"description": "Alert Bell Buzzer" "description": "Alert Bell Buzzer"
}, },
"usePwm": { "usePwm": {
"label": "Use PWM", "label": "Use PWM buzzer",
"description": "Use PWM" "description": "Use PWM"
}, },
"nagTimeout": { "nagTimeout": {
"label": "Nag Timeout", "label": "Nag timeout (seconds)",
"description": "Nag Timeout" "description": "Nag Timeout"
}, },
"useI2sAsBuzzer": { "useI2sAsBuzzer": {
"label": "Use I²S Pin as Buzzer", "label": "Use I2S as buzzer",
"description": "Designate I²S Pin as Buzzer Output" "description": "Use I2S output as buzzer driver"
},
"externalNotificationConfig": {
"label": "External Notification Config",
"description": "External notification configuration"
},
"notificationsOnMessage": {
"label": "Notifications on message receipt",
"description": "Alerts on incoming text messages"
},
"notificationsOnAlert": {
"label": "Notifications on alert/bell receipt",
"description": "Alerts on incoming bell/alert packets"
},
"advanced": {
"label": "Advanced",
"description": "Pin assignments and timing"
} }
}, },
"mqtt": { "mqtt": {
"title": "MQTT Settings", "title": "MQTT",
"description": "Settings for the MQTT module", "description": "MQTT module configuration",
"enabled": { "enabled": {
"label": "Enabled", "label": "MQTT enabled",
"description": "Enable or disable MQTT" "description": "Enable or disable MQTT"
}, },
"address": { "address": {
"label": "MQTT Server Address", "label": "Address",
"description": "MQTT server address to use for default/custom servers" "description": "MQTT server address to use for default/custom servers"
}, },
"username": { "username": {
"label": "MQTT Username", "label": "Username",
"description": "MQTT username to use for default/custom servers" "description": "MQTT username to use for default/custom servers"
}, },
"password": { "password": {
"label": "MQTT Password", "label": "Password",
"description": "MQTT password to use for default/custom servers" "description": "MQTT password to use for default/custom servers"
}, },
"encryptionEnabled": { "encryptionEnabled": {
"label": "Encryption Enabled", "label": "Encryption enabled",
"description": "Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data." "description": "Enable or disable MQTT encryption. Note: All messages are sent to the MQTT broker unencrypted if this option is not enabled, even when your uplink channels have encryption keys set. This includes position data."
}, },
"jsonEnabled": { "jsonEnabled": {
"label": "JSON Enabled", "label": "JSON output enabled",
"description": "Whether to send/consume JSON packets on MQTT" "description": "Whether to send/consume JSON packets on MQTT"
}, },
"tlsEnabled": { "tlsEnabled": {
"label": "TLS Enabled", "label": "TLS enabled",
"description": "Enable or disable TLS" "description": "Enable or disable TLS"
}, },
"root": { "root": {
@@ -253,20 +289,20 @@
"description": "MQTT root topic to use for default/custom servers" "description": "MQTT root topic to use for default/custom servers"
}, },
"proxyToClientEnabled": { "proxyToClientEnabled": {
"label": "MQTT Client Proxy Enabled", "label": "Proxy to client enabled",
"description": "Utilizes the network connection to proxy MQTT messages to the client." "description": "Utilizes the network connection to proxy MQTT messages to the client."
}, },
"mapReportingEnabled": { "mapReportingEnabled": {
"label": "Map Reporting Enabled", "label": "Map reporting",
"description": "Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, short and long name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name." "description": "Your node will periodically send an unencrypted map report packet to the configured MQTT server, this includes id, long and short name, approximate location, hardware model, role, firmware version, LoRa region, modem preset and primary channel name."
}, },
"mapReportSettings": { "mapReportSettings": {
"publishIntervalSecs": { "publishIntervalSecs": {
"label": "Map Report Publish Interval (s)", "label": "Map reporting interval (seconds)",
"description": "Interval in seconds to publish map reports" "description": "Interval in seconds to publish map reports"
}, },
"positionPrecision": { "positionPrecision": {
"label": "Approximate Location", "label": "Approximate location",
"description": "Position shared will be accurate within this distance", "description": "Position shared will be accurate within this distance",
"options": { "options": {
"metric_km23": "Within 23 km", "metric_km23": "Within 23 km",
@@ -290,78 +326,108 @@
"imperial_ft300": "Within 300 feet", "imperial_ft300": "Within 300 feet",
"imperial_ft150": "Within 150 feet" "imperial_ft150": "Within 150 feet"
} }
},
"shouldReportLocation": {
"label": "I agree.",
"description": "I have read and understand the above. I voluntarily consent to the unencrypted transmission of my node data via MQTT.",
"consentHeader": "Consent to Share Unencrypted Node Data via MQTT",
"consentText": "By enabling this feature, you acknowledge and expressly consent to the transmission of your device's real-time geographic location over the MQTT protocol without encryption. This location data may be used for purposes such as live map reporting, device tracking, and related telemetry functions."
} }
},
"mqttConfigCard": {
"label": "MQTT Config",
"description": "MQTT broker connection"
},
"mapReportingCard": {
"label": "Map reporting",
"description": "Periodic unencrypted map report"
} }
}, },
"neighborInfo": { "neighborInfo": {
"title": "Neighbor Info Settings", "title": "Neighbor Info",
"description": "Settings for the Neighbor Info module", "description": "Settings for the Neighbor Info module",
"enabled": { "enabled": {
"label": "Enabled", "label": "Neighbor Info enabled",
"description": "Enable or disable Neighbor Info Module" "description": "Enable or disable Neighbor Info Module"
}, },
"updateInterval": { "updateInterval": {
"label": "Update Interval", "label": "Update interval (seconds)",
"description": "Interval in seconds of how often we should try to send our Neighbor Info to the mesh" "description": "Interval in seconds of how often we should try to send our Neighbor Info to the mesh"
},
"neighborInfoConfig": {
"label": "Neighbor Info Config",
"description": "Neighbor Info configuration"
},
"transmitOverLora": {
"label": "Transmit over LoRa",
"description": "Whether NeighborInfo should be transmitted over LoRa. Not available on a channel with default key and name."
} }
}, },
"paxcounter": { "paxcounter": {
"title": "Paxcounter Settings", "title": "Paxcounter",
"description": "Settings for the Paxcounter module", "description": "Settings for the Paxcounter module",
"enabled": { "enabled": {
"label": "Module Enabled", "label": "Paxcounter enabled",
"description": "Enable Paxcounter" "description": "Enable Paxcounter"
}, },
"paxcounterUpdateInterval": { "paxcounterUpdateInterval": {
"label": "Update Interval (seconds)", "label": "Update interval (seconds)",
"description": "How long to wait between sending paxcounter packets" "description": "How long to wait between sending paxcounter packets"
}, },
"wifiThreshold": { "wifiThreshold": {
"label": "WiFi RSSI Threshold", "label": "WiFi RSSI threshold (defaults to -80)",
"description": "At what WiFi RSSI level should the counter increase. Defaults to -80." "description": "At what WiFi RSSI level should the counter increase. Defaults to -80."
}, },
"bleThreshold": { "bleThreshold": {
"label": "BLE RSSI Threshold", "label": "BLE RSSI threshold (defaults to -80)",
"description": "At what BLE RSSI level should the counter increase. Defaults to -80." "description": "At what BLE RSSI level should the counter increase. Defaults to -80."
},
"paxcounterConfig": {
"label": "Paxcounter Config",
"description": "Paxcounter configuration"
} }
}, },
"rangeTest": { "rangeTest": {
"title": "Range Test Settings", "title": "Range Test",
"description": "Settings for the Range Test module", "description": "Settings for the Range Test module",
"enabled": { "enabled": {
"label": "Module Enabled", "label": "Range test enabled",
"description": "Enable Range Test" "description": "Enable Range Test"
}, },
"sender": { "sender": {
"label": "Message Interval", "label": "Sender message interval (seconds)",
"description": "How long to wait between sending test packets" "description": "How long to wait between sending test packets"
}, },
"save": { "save": {
"label": "Save CSV to storage", "label": "Save .CSV in storage (ESP32 only)",
"description": "ESP32 Only" "description": "ESP32 Only"
},
"rangeTestConfig": {
"label": "Range Test Config",
"description": "Range Test configuration"
} }
}, },
"serial": { "serial": {
"title": "Serial Settings", "title": "Serial",
"description": "Settings for the Serial module", "description": "Settings for the Serial module",
"enabled": { "enabled": {
"label": "Module Enabled", "label": "Serial enabled",
"description": "Enable Serial output" "description": "Enable Serial output"
}, },
"echo": { "echo": {
"label": "Echo", "label": "Echo enabled",
"description": "Any packets you send will be echoed back to your device" "description": "Any packets you send will be echoed back to your device"
}, },
"rxd": { "rxd": {
"label": "Receive Pin", "label": "RX",
"description": "Set the GPIO pin to the RXD pin you have set up." "description": "Set the GPIO pin to the RXD pin you have set up."
}, },
"txd": { "txd": {
"label": "Transmit Pin", "label": "TX",
"description": "Set the GPIO pin to the TXD pin you have set up." "description": "Set the GPIO pin to the TXD pin you have set up."
}, },
"baud": { "baud": {
"label": "Baud Rate", "label": "Serial baud rate",
"description": "The serial baud rate" "description": "The serial baud rate"
}, },
"timeout": { "timeout": {
@@ -369,23 +435,27 @@
"description": "Seconds to wait before we consider your packet as 'done'" "description": "Seconds to wait before we consider your packet as 'done'"
}, },
"mode": { "mode": {
"label": "Mode", "label": "Serial mode",
"description": "Select Mode" "description": "Select Mode"
}, },
"overrideConsoleSerialPort": { "overrideConsoleSerialPort": {
"label": "Override Console Serial Port", "label": "Override console serial port",
"description": "If you have a serial port connected to the console, this will override it." "description": "If you have a serial port connected to the console, this will override it."
},
"serialConfig": {
"label": "Serial Config",
"description": "Serial module configuration"
} }
}, },
"storeForward": { "storeForward": {
"title": "Store & Forward Settings", "title": "Store & Forward",
"description": "Settings for the Store & Forward module", "description": "Settings for the Store & Forward module",
"enabled": { "enabled": {
"label": "Module Enabled", "label": "Store & Forward enabled",
"description": "Enable Store & Forward" "description": "Enable Store & Forward"
}, },
"heartbeat": { "heartbeat": {
"label": "Heartbeat Enabled", "label": "Heartbeat",
"description": "Enable Store & Forward heartbeat" "description": "Enable Store & Forward heartbeat"
}, },
"records": { "records": {
@@ -399,50 +469,66 @@
"historyReturnWindow": { "historyReturnWindow": {
"label": "History return window", "label": "History return window",
"description": "Return records from this time window (minutes)" "description": "Return records from this time window (minutes)"
},
"storeForwardConfig": {
"label": "Store & Forward Config",
"description": "Store & Forward configuration"
},
"isServer": {
"label": "Server",
"description": "Run as a Store & Forward server"
} }
}, },
"telemetry": { "telemetry": {
"title": "Telemetry Settings", "title": "Telemetry",
"description": "Settings for the Telemetry module", "description": "Settings for the Telemetry module",
"deviceUpdateInterval": { "deviceUpdateInterval": {
"label": "Device Metrics", "label": "Device metrics update interval",
"description": "Device metrics update interval (seconds)" "description": "Device metrics update interval (seconds)"
}, },
"environmentUpdateInterval": { "environmentUpdateInterval": {
"label": "Environment metrics update interval (seconds)", "label": "Environment metrics update interval",
"description": "" "description": ""
}, },
"environmentMeasurementEnabled": { "environmentMeasurementEnabled": {
"label": "Module Enabled", "label": "Environment metrics module enabled",
"description": "Enable the Environment Telemetry" "description": "Enable the Environment Telemetry"
}, },
"environmentScreenEnabled": { "environmentScreenEnabled": {
"label": "Displayed on Screen", "label": "Environment metrics on-screen enabled",
"description": "Show the Telemetry Module on the OLED" "description": "Show the Telemetry Module on the OLED"
}, },
"environmentDisplayFahrenheit": { "environmentDisplayFahrenheit": {
"label": "Display Fahrenheit", "label": "Environment metrics use Fahrenheit",
"description": "Display temp in Fahrenheit" "description": "Display temp in Fahrenheit"
}, },
"airQualityEnabled": { "airQualityEnabled": {
"label": "Air Quality Enabled", "label": "Air quality metrics module enabled",
"description": "Enable the Air Quality Telemetry" "description": "Enable the Air Quality Telemetry"
}, },
"airQualityInterval": { "airQualityInterval": {
"label": "Air Quality Update Interval", "label": "Air quality metrics update interval",
"description": "How often to send Air Quality data over the mesh" "description": "How often to send Air Quality data over the mesh"
}, },
"powerMeasurementEnabled": { "powerMeasurementEnabled": {
"label": "Power Measurement Enabled", "label": "Power metrics module enabled",
"description": "Enable the Power Measurement Telemetry" "description": "Enable the Power Measurement Telemetry"
}, },
"powerUpdateInterval": { "powerUpdateInterval": {
"label": "Power Update Interval", "label": "Power metrics update interval",
"description": "How often to send Power data over the mesh" "description": "How often to send Power data over the mesh"
}, },
"powerScreenEnabled": { "powerScreenEnabled": {
"label": "Power Screen Enabled", "label": "Power metrics on-screen enabled",
"description": "Enable the Power Telemetry Screen" "description": "Show power metrics on the device screen"
},
"telemetryConfig": {
"label": "Telemetry Config",
"description": "Telemetry configuration"
},
"deviceTelemetryEnabled": {
"label": "Send Device Telemetry",
"description": "Enable/Disable the device telemetry module to send metrics to the mesh."
} }
} }
} }
@@ -3,6 +3,7 @@
"apply": "Apply", "apply": "Apply",
"addConnection": "Add Connection", "addConnection": "Add Connection",
"saveConnection": "Save connection", "saveConnection": "Save connection",
"saveChanges": "Save changes",
"backupKey": "Backup Key", "backupKey": "Backup Key",
"cancel": "Cancel", "cancel": "Cancel",
"connect": "Connect", "connect": "Connect",
+179 -74
View File
@@ -16,15 +16,15 @@
"label": "Configuration" "label": "Configuration"
}, },
"device": { "device": {
"title": "Device Settings", "title": "Device",
"description": "Settings for the device", "description": "Settings for the device",
"buttonPin": { "buttonPin": {
"description": "Button pin override", "description": "Button pin override",
"label": "Button Pin" "label": "Button GPIO"
}, },
"buzzerPin": { "buzzerPin": {
"description": "Buzzer pin override", "description": "Buzzer pin override",
"label": "Buzzer Pin" "label": "Buzzer GPIO"
}, },
"disableTripleClick": { "disableTripleClick": {
"description": "Disable triple click", "description": "Disable triple click",
@@ -43,8 +43,8 @@
"label": "Node Info Broadcast Interval" "label": "Node Info Broadcast Interval"
}, },
"posixTimezone": { "posixTimezone": {
"description": "The POSIX timezone string for the device", "description": "Time zone for dates on the device screen and log.",
"label": "POSIX Timezone" "label": "Time zone"
}, },
"rebroadcastMode": { "rebroadcastMode": {
"description": "How to handle rebroadcasting", "description": "How to handle rebroadcasting",
@@ -52,52 +52,75 @@
}, },
"role": { "role": {
"description": "What role the device performs on the mesh", "description": "What role the device performs on the mesh",
"label": "Role" "label": "Device Role"
},
"options": {
"label": "Options",
"description": "Device role, rebroadcast mode, and node-info broadcast cadence"
},
"hardware": {
"label": "Hardware",
"description": "Hardware-related toggles"
},
"timeZone": {
"label": "Time Zone",
"description": "POSIX time zone for dates on the device screen and log"
},
"gpio": {
"label": "GPIO",
"description": "GPIO pin overrides"
},
"useBrowserTimeZone": {
"label": "Use browser time zone"
} }
}, },
"bluetooth": { "bluetooth": {
"title": "Bluetooth Settings", "title": "Bluetooth",
"description": "Settings for the Bluetooth module", "description": "Settings for the Bluetooth module",
"note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.",
"enabled": { "enabled": {
"description": "Enable or disable Bluetooth", "description": "Enable or disable Bluetooth",
"label": "Enabled" "label": "Bluetooth enabled"
}, },
"pairingMode": { "pairingMode": {
"description": "Pin selection behaviour.", "description": "Pin selection behaviour.",
"label": "Pairing mode" "label": "Pairing mode"
}, },
"pin": { "pin": {
"description": "Pin to use when pairing", "description": "PIN to use when pairing in fixed-PIN mode",
"label": "Pin" "label": "Fixed PIN"
},
"bluetoothConfig": {
"label": "Bluetooth Config",
"description": "Bluetooth configuration"
} }
}, },
"display": { "display": {
"description": "Settings for the device display", "description": "Settings for the device display",
"title": "Display Settings", "title": "Display",
"headingBold": { "headingBold": {
"description": "Bolden the heading text", "description": "Bolden the heading text",
"label": "Bold Heading" "label": "Bold Heading"
}, },
"carouselDelay": { "carouselDelay": {
"description": "How fast to cycle through windows", "description": "How fast to cycle through windows",
"label": "Carousel Delay" "label": "Carousel interval"
}, },
"compassNorthTop": { "compassNorthTop": {
"description": "Fix north to the top of compass", "description": "Fix north to the top of compass",
"label": "Compass North Top" "label": "Always point north"
}, },
"displayMode": { "displayMode": {
"description": "Screen layout variant", "description": "Screen layout variant",
"label": "Display Mode" "label": "Display mode"
}, },
"displayUnits": { "displayUnits": {
"description": "Display metric or imperial units", "description": "Display metric or imperial units",
"label": "Display Units" "label": "Display units"
}, },
"flipScreen": { "flipScreen": {
"description": "Flip display 180 degrees", "description": "Flip display 180 degrees",
"label": "Flip Screen" "label": "Flip screen"
}, },
"gpsDisplayUnits": { "gpsDisplayUnits": {
"description": "Coordinate display format", "description": "Coordinate display format",
@@ -105,35 +128,47 @@
}, },
"oledType": { "oledType": {
"description": "Type of OLED screen attached to the device", "description": "Type of OLED screen attached to the device",
"label": "OLED Type" "label": "OLED type"
}, },
"screenTimeout": { "screenTimeout": {
"description": "Turn off the display after this long", "description": "Turn off the display after this long",
"label": "Screen Timeout" "label": "Screen on for"
}, },
"twelveHourClock": { "twelveHourClock": {
"description": "Use 12-hour clock format", "description": "Display time in 12h format",
"label": "12-Hour Clock" "label": "Use 12h clock format"
}, },
"wakeOnTapOrMotion": { "wakeOnTapOrMotion": {
"description": "Wake the device on tap or motion", "description": "Wake the device on tap or motion",
"label": "Wake on Tap or Motion" "label": "Wake on tap or motion"
}, },
"enableMessageBubbles": { "enableMessageBubbles": {
"label": "Message Bubbles", "label": "Message Bubbles",
"description": "Display incoming messages as bubbles on the device screen" "description": "Display incoming messages as bubbles on the device screen"
},
"deviceDisplay": {
"label": "Device Display",
"description": "Display orientation and clock"
},
"advanced": {
"label": "Advanced",
"description": "Advanced display settings"
},
"compassOrientation": {
"label": "Compass orientation",
"description": "Compass orientation relative to the screen"
} }
}, },
"lora": { "lora": {
"title": "Mesh Settings", "title": "LoRa",
"description": "Settings for the LoRa mesh", "description": "LoRa radio configuration",
"bandwidth": { "bandwidth": {
"description": "Channel bandwidth in kHz", "description": "Channel bandwidth in kHz",
"label": "Bandwidth" "label": "Bandwidth"
}, },
"boostedRxGain": { "boostedRxGain": {
"description": "Boosted RX gain", "description": "Boosted RX gain",
"label": "Boosted RX Gain" "label": "RX Boosted Gain"
}, },
"codingRate": { "codingRate": {
"description": "The denominator of the coding rate", "description": "The denominator of the coding rate",
@@ -144,24 +179,24 @@
"label": "Frequency Offset" "label": "Frequency Offset"
}, },
"frequencySlot": { "frequencySlot": {
"description": "LoRa frequency channel number", "description": "Your node's operating frequency is calculated based on the region, modem preset, and this field. When 0, the slot is automatically calculated based on the primary channel name and will change from the default public slot.",
"label": "Frequency Slot" "label": "Frequency Slot"
}, },
"hopLimit": { "hopLimit": {
"description": "Maximum number of hops", "description": "Sets the maximum number of hops, default is 3. Increasing hops also increases congestion and should be used carefully. 0 hop broadcast messages will not get ACKs.",
"label": "Hop Limit" "label": "Number of Hops"
}, },
"ignoreMqtt": { "ignoreMqtt": {
"description": "Don't forward MQTT messages over the mesh", "description": "Don't forward MQTT messages over the mesh",
"label": "Ignore MQTT" "label": "Ignore MQTT"
}, },
"modemPreset": { "modemPreset": {
"description": "Modem preset to use", "description": "Available modem presets, default is Long Fast.",
"label": "Modem Preset" "label": "Presets"
}, },
"okToMqtt": { "okToMqtt": {
"description": "When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT", "description": "When set to true, this configuration indicates that the user approves the packet to be uploaded to MQTT. If set to false, remote nodes are requested not to forward packets to MQTT",
"label": "OK to MQTT" "label": "Ok to MQTT"
}, },
"overrideDutyCycle": { "overrideDutyCycle": {
"description": "Override Duty Cycle", "description": "Override Duty Cycle",
@@ -169,10 +204,10 @@
}, },
"overrideFrequency": { "overrideFrequency": {
"description": "Override frequency", "description": "Override frequency",
"label": "Override Frequency" "label": "Frequency Override"
}, },
"region": { "region": {
"description": "Sets the region for your node", "description": "The region where you will be using your radios.",
"label": "Region" "label": "Region"
}, },
"spreadingFactor": { "spreadingFactor": {
@@ -210,15 +245,23 @@
"serialHalOnly": { "serialHalOnly": {
"label": "Serial HAL Only", "label": "Serial HAL Only",
"description": "Communicate with the radio exclusively over the serial hardware abstraction layer" "description": "Communicate with the radio exclusively over the serial hardware abstraction layer"
},
"optionsCard": {
"label": "Options",
"description": "LoRa core options"
},
"advancedCard": {
"label": "Advanced",
"description": "Advanced LoRa parameters"
} }
}, },
"network": { "network": {
"title": "WiFi Config", "title": "Network",
"description": "WiFi radio configuration", "description": "Network configuration",
"note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.", "note": "Note: Some devices (ESP32) cannot use both Bluetooth and WiFi at the same time.",
"addressMode": { "addressMode": {
"description": "Address assignment selection", "description": "Address assignment selection",
"label": "Address Mode" "label": "IPv4 mode"
}, },
"dns": { "dns": {
"description": "DNS Server", "description": "DNS Server",
@@ -226,7 +269,7 @@
}, },
"ethernetEnabled": { "ethernetEnabled": {
"description": "Enable or disable the Ethernet port", "description": "Enable or disable the Ethernet port",
"label": "Enabled" "label": "Ethernet enabled"
}, },
"gateway": { "gateway": {
"description": "Default Gateway", "description": "Default Gateway",
@@ -238,7 +281,7 @@
}, },
"psk": { "psk": {
"description": "Network password", "description": "Network password",
"label": "PSK" "label": "Password"
}, },
"ssid": { "ssid": {
"description": "Network name", "description": "Network name",
@@ -250,16 +293,17 @@
}, },
"wifiEnabled": { "wifiEnabled": {
"description": "Enable or disable the WiFi radio", "description": "Enable or disable the WiFi radio",
"label": "Enabled" "label": "WiFi enabled"
}, },
"meshViaUdp": { "meshViaUdp": {
"label": "Mesh via UDP" "label": "Enabled",
"description": "Forward mesh packets over UDP"
}, },
"ntpServer": { "ntpServer": {
"label": "NTP Server" "label": "NTP server"
}, },
"rsyslogServer": { "rsyslogServer": {
"label": "Rsyslog Server" "label": "rsyslog server"
}, },
"ethernetConfigSettings": { "ethernetConfigSettings": {
"description": "Ethernet port configuration", "description": "Ethernet port configuration",
@@ -280,18 +324,30 @@
"udpConfigSettings": { "udpConfigSettings": {
"description": "UDP over Mesh configuration", "description": "UDP over Mesh configuration",
"label": "UDP Config" "label": "UDP Config"
},
"wifiOptions": {
"label": "WiFi Options",
"description": "WiFi configuration"
},
"ethernetOptionsCard": {
"label": "Ethernet Options",
"description": "Ethernet configuration"
},
"advancedCard": {
"label": "Advanced",
"description": "NTP, rsyslog, IPv4 mode and address overrides"
} }
}, },
"position": { "position": {
"title": "Position Settings", "title": "Position",
"description": "Settings for the position module", "description": "Position settings",
"broadcastInterval": { "broadcastInterval": {
"description": "How often your position is sent out over the mesh", "description": "The maximum interval that can elapse without a node broadcasting a position.",
"label": "Broadcast Interval" "label": "Broadcast Interval"
}, },
"enablePin": { "enablePin": {
"description": "GPS module enable pin override", "description": "GPS enable GPIO pin override",
"label": "Enable Pin" "label": "GPS EN GPIO"
}, },
"fixedPosition": { "fixedPosition": {
"description": "Don't report GPS position, but a manually-specified one", "description": "Don't report GPS position, but a manually-specified one",
@@ -311,35 +367,35 @@
}, },
"gpsMode": { "gpsMode": {
"description": "Configure whether device GPS is Enabled, Disabled, or Not Present", "description": "Configure whether device GPS is Enabled, Disabled, or Not Present",
"label": "GPS Mode" "label": "GPS Mode (Physical Hardware)"
}, },
"gpsUpdateInterval": { "gpsUpdateInterval": {
"description": "How often a GPS fix should be acquired", "description": "How often should we try to get a GPS position (<10sec keeps GPS on).",
"label": "GPS Update Interval" "label": "GPS Polling Interval"
}, },
"positionFlags": { "positionFlags": {
"description": "Optional fields to include when assembling position messages. The more fields are selected, the larger the message will be leading to longer airtime usage and a higher risk of packet loss.", "description": "Optional fields to include when assembling position messages. The more fields are included, the larger the message will be — leading to longer airtime and a higher risk of packet loss.",
"label": "Position Flags" "label": "Position Flags"
}, },
"receivePin": { "receivePin": {
"description": "GPS module RX pin override", "description": "GPS receive GPIO pin override",
"label": "Receive Pin" "label": "GPS Receive GPIO"
}, },
"smartPositionEnabled": { "smartPositionEnabled": {
"description": "Only send position when there has been a meaningful change in location", "description": "Only send position when there has been a meaningful change in location",
"label": "Enable Smart Position" "label": "Smart Position"
}, },
"smartPositionMinDistance": { "smartPositionMinDistance": {
"description": "Minimum distance (in meters) that must be traveled before a position update is sent", "description": "The minimum distance change in meters to be considered for a smart position broadcast.",
"label": "Smart Position Minimum Distance" "label": "Smart Distance"
}, },
"smartPositionMinInterval": { "smartPositionMinInterval": {
"description": "Minimum interval (in seconds) that must pass before a position update is sent", "description": "The fastest that position updates will be sent if the minimum distance has been satisfied.",
"label": "Smart Position Minimum Interval" "label": "Smart Interval"
}, },
"transmitPin": { "transmitPin": {
"description": "GPS module TX pin override", "description": "GPS transmit GPIO pin override",
"label": "Transmit Pin" "label": "GPS Transmit GPIO"
}, },
"intervalsSettings": { "intervalsSettings": {
"description": "How often to send position updates", "description": "How often to send position updates",
@@ -358,16 +414,33 @@
"unset": "Unset", "unset": "Unset",
"vehicleHeading": "Vehicle heading", "vehicleHeading": "Vehicle heading",
"vehicleSpeed": "Vehicle speed" "vehicleSpeed": "Vehicle speed"
},
"positionPacket": {
"label": "Position Packet",
"description": "Position broadcast configuration"
},
"deviceGps": {
"label": "Device GPS",
"description": "Device GPS configuration"
},
"advancedDeviceGps": {
"label": "Advanced Device GPS",
"description": "GPIO pin overrides for the GPS hardware"
},
"useBrowserLocation": {
"label": "Use browser location",
"busy": "Reading location…",
"failed": "Could not read browser location"
} }
}, },
"power": { "power": {
"adcMultiplierOverride": { "adcMultiplierOverride": {
"description": "Used for tweaking battery voltage reading", "description": "Used for tweaking battery voltage reading",
"label": "ADC Multiplier Override ratio" "label": "ADC multiplier override ratio"
}, },
"ina219Address": { "ina219Address": {
"description": "Address of the INA219 battery monitor", "description": "Address of the INA219 battery monitor",
"label": "INA219 Address" "label": "Battery INA_2XX I2C address"
}, },
"lightSleepDuration": { "lightSleepDuration": {
"description": "How long the device will be in light sleep for", "description": "How long the device will be in light sleep for",
@@ -375,11 +448,11 @@
}, },
"minimumWakeTime": { "minimumWakeTime": {
"description": "Minimum amount of time the device will stay awake for after receiving a packet", "description": "Minimum amount of time the device will stay awake for after receiving a packet",
"label": "Minimum Wake Time" "label": "Minimum wake time"
}, },
"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": "Wait for Bluetooth duration"
}, },
"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.",
@@ -387,11 +460,11 @@
}, },
"shutdownOnBatteryDelay": { "shutdownOnBatteryDelay": {
"description": "Automatically shutdown node after this long when on battery, 0 for indefinite", "description": "Automatically shutdown node after this long when on battery, 0 for indefinite",
"label": "Shutdown on battery delay" "label": "Shutdown on power loss"
}, },
"superDeepSleepDuration": { "superDeepSleepDuration": {
"description": "How long the device will be in super deep sleep for", "description": "How long the device will be in super deep sleep for",
"label": "Super Deep Sleep Duration" "label": "Super deep sleep duration"
}, },
"powerConfigSettings": { "powerConfigSettings": {
"description": "Settings for the power module", "description": "Settings for the power module",
@@ -400,23 +473,27 @@
"sleepSettings": { "sleepSettings": {
"description": "Sleep settings for the power module", "description": "Sleep settings for the power module",
"label": "Sleep Settings" "label": "Sleep Settings"
},
"powerConfig": {
"label": "Power Config",
"description": "Power and sleep configuration"
} }
}, },
"security": { "security": {
"description": "Settings for the Security configuration", "description": "Settings for the Security configuration",
"title": "Security Settings", "title": "Security",
"button_backupKey": "Backup Key", "button_backupKey": "Backup Key",
"adminChannelEnabled": { "adminChannelEnabled": {
"description": "Allow incoming device control over the insecure legacy admin channel", "description": "Allow incoming device control over the insecure legacy admin channel",
"label": "Allow Legacy Admin" "label": "Legacy Admin channel"
}, },
"enableDebugLogApi": { "enableDebugLogApi": {
"description": "Output live debug logging over serial, view and export position-redacted device logs over Bluetooth", "description": "Output live debug logging over serial, view and export position-redacted device logs over Bluetooth",
"label": "Enable Debug Log API" "label": "Debug log API enabled"
}, },
"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": "Managed Mode"
}, },
"privateKey": { "privateKey": {
"description": "Used to create a shared key with a remote device", "description": "Used to create a shared key with a remote device",
@@ -436,7 +513,7 @@
}, },
"serialOutputEnabled": { "serialOutputEnabled": {
"description": "Serial Console over the Stream API", "description": "Serial Console over the Stream API",
"label": "Serial Output Enabled" "label": "Serial console"
}, },
"tertiaryAdminKey": { "tertiaryAdminKey": {
"description": "The tertiary public key authorized to send admin messages to this node", "description": "The tertiary public key authorized to send admin messages to this node",
@@ -449,10 +526,26 @@
"loggingSettings": { "loggingSettings": {
"description": "Settings for Logging", "description": "Settings for Logging",
"label": "Logging Settings" "label": "Logging Settings"
},
"directMessageKey": {
"label": "Direct Message Key",
"description": "Public + private direct-message keys"
},
"adminKeysCard": {
"label": "Admin Keys",
"description": "Admin authentication keys"
},
"logsCard": {
"label": "Logs",
"description": "Logging configuration"
},
"administration": {
"label": "Administration",
"description": "Managed-mode and legacy admin channel"
} }
}, },
"user": { "user": {
"title": "User Settings", "title": "User",
"description": "Configure your device name and identity settings", "description": "Configure your device name and identity settings",
"longName": { "longName": {
"label": "Long Name", "label": "Long Name",
@@ -472,11 +565,23 @@
}, },
"isUnmessageable": { "isUnmessageable": {
"label": "Unmessageable", "label": "Unmessageable",
"description": "Used to identify unmonitored or infrastructure nodes so that messaging is not available to nodes that will never respond." "description": "Unmonitored or Infrastructure"
}, },
"isLicensed": { "isLicensed": {
"label": "Licensed amateur radio (HAM)", "label": "Licensed amateur radio (Ham)",
"description": "Enable if you are a licensed amateur radio operator, enabling this option disables encryption and is not compatible with the default Meshtastic network." "description": "Enabling this option disables encryption and is not compatible with the default Meshtastic network."
},
"userConfig": {
"label": "User Config",
"description": "Identity and licensing"
},
"nodeId": {
"label": "Node ID",
"description": "Read-only — assigned by the device"
},
"hardwareModel": {
"label": "Hardware model",
"description": "Read-only — reported by the device"
} }
} }
} }
@@ -15,6 +15,26 @@
}, },
"lastConnectedAt": "Last connected: {{date}}", "lastConnectedAt": "Last connected: {{date}}",
"neverConnected": "Never connected", "neverConnected": "Never connected",
"default": "Default",
"button": {
"addConnection": "Add Connection",
"connect": "Connect",
"disconnect": "Disconnect",
"retry": "Retry",
"delete": "Delete",
"cancel": "Cancel",
"setDefault": "Set as default",
"unsetDefault": "Unset default",
"rename": "Rename",
"edit": "Edit"
},
"renameConnection": "Rename connection",
"renameDescription": "Enter a new name for this connection.",
"nameLabel": "Name",
"editConnection": {
"title": "Edit Connection",
"description": "Modify the settings for this connection."
},
"toasts": { "toasts": {
"connected": "Connected", "connected": "Connected",
"nowConnected": "{{name}} is now connected", "nowConnected": "{{name}} is now connected",
@@ -29,6 +49,25 @@
"pickConnectionAgain": "Could not connect. You may need to reselect the device/port.", "pickConnectionAgain": "Could not connect. You may need to reselect the device/port.",
"added": "Connection added", "added": "Connection added",
"savedByName": "{{name}} saved.", "savedByName": "{{name}} saved.",
"savedCantConnect": "The connection was saved but could not connect." "savedCantConnect": "The connection was saved but could not connect.",
"renamed": "Renamed",
"renamedByName": "Connection renamed to {{name}}.",
"updated": "Updated",
"updatedByName": "{{name}} has been updated."
},
"overlay": {
"srTitle": "Connecting to device",
"phase": {
"connecting": "Opening transport…",
"configuring": "Streaming configuration…"
},
"chip": {
"identity": "Identity",
"metadata": "Metadata",
"channels": "Channels",
"nodes": "Nodes"
},
"stuckHint": "Taking longer than usual. The device may be in CLI / bootloader mode, or the firmware isn't responding to config requests.",
"cancel": "Cancel"
} }
} }
@@ -79,6 +79,7 @@
"inputPlaceholder": "192.168.1.10 or meshtastic.local", "inputPlaceholder": "192.168.1.10 or meshtastic.local",
"heading": "URL or IP", "heading": "URL or IP",
"useHttps": "Use HTTPS", "useHttps": "Use HTTPS",
"mixedContentWarning": "This page is loaded over HTTPS. Your browser may block connections to an HTTP node.",
"invalidUrl": { "invalidUrl": {
"title": "Invalid URL", "title": "Invalid URL",
"description": "Please enter a valid HTTP or HTTPS URL." "description": "Please enter a valid HTTP or HTTPS URL."
@@ -148,6 +149,11 @@
"remindNever": "Never remind me", "remindNever": "Never remind me",
"backupNow": "Back up now" "backupNow": "Back up now"
}, },
"regionSetup": {
"title": "Set your region",
"description": "This device has no LoRa region configured and won't transmit until you pick one.",
"cta": "Set region"
},
"pkiRegenerate": { "pkiRegenerate": {
"description": "Are you sure you want to regenerate key pair?", "description": "Are you sure you want to regenerate key pair?",
"title": "Regenerate Key Pair" "title": "Regenerate Key Pair"
@@ -39,6 +39,10 @@
"blue": { "blue": {
"label": "Blue", "label": "Blue",
"description": "Sets the blue LED level. Values are 0-255" "description": "Sets the blue LED level. Values are 0-255"
},
"ambientLightingConfig": {
"label": "Ambient Lighting Config",
"description": "Ambient Lighting configuration"
} }
}, },
"audio": { "audio": {
@@ -71,6 +75,10 @@
"i2sSck": { "i2sSck": {
"label": "i2S SCK", "label": "i2S SCK",
"description": "GPIO pin to use for i2S SCK" "description": "GPIO pin to use for i2S SCK"
},
"audioConfig": {
"label": "Audio Config",
"description": "Audio configuration"
} }
}, },
"cannedMessage": { "cannedMessage": {
@@ -119,6 +127,14 @@
"sendBell": { "sendBell": {
"label": "Send Bell", "label": "Send Bell",
"description": "Sends a bell character with each message" "description": "Sends a bell character with each message"
},
"cannedMessageConfig": {
"label": "Canned Message Config",
"description": "Canned Message configuration"
},
"enabled": {
"label": "Canned message enabled",
"description": "Enable Canned Message"
} }
}, },
"detectionSensor": { "detectionSensor": {
@@ -155,6 +171,10 @@
"usePullup": { "usePullup": {
"label": "Use Pullup", "label": "Use Pullup",
"description": "Whether or not use INPUT_PULLUP mode for GPIO pin" "description": "Whether or not use INPUT_PULLUP mode for GPIO pin"
},
"detectionSensorConfig": {
"label": "Detection Sensor Config",
"description": "Detection Sensor configuration"
} }
}, },
"externalNotification": { "externalNotification": {
@@ -219,6 +239,22 @@
"useI2sAsBuzzer": { "useI2sAsBuzzer": {
"label": "Use I²S Pin as Buzzer", "label": "Use I²S Pin as Buzzer",
"description": "Designate I²S Pin as Buzzer Output" "description": "Designate I²S Pin as Buzzer Output"
},
"externalNotificationConfig": {
"label": "External Notification Config",
"description": "External notification configuration"
},
"notificationsOnMessage": {
"label": "Notifications on message receipt",
"description": "Alerts on incoming text messages"
},
"notificationsOnAlert": {
"label": "Notifications on alert/bell receipt",
"description": "Alerts on incoming bell/alert packets"
},
"advanced": {
"label": "Advanced",
"description": "Pin assignments and timing"
} }
}, },
"mqtt": { "mqtt": {
@@ -294,7 +330,21 @@
"imperial_ft300": "Within 300 feet", "imperial_ft300": "Within 300 feet",
"imperial_ft150": "Within 150 feet" "imperial_ft150": "Within 150 feet"
} }
},
"shouldReportLocation": {
"label": "I agree.",
"description": "I have read and understand the above. I voluntarily consent to the unencrypted transmission of my node data via MQTT.",
"consentHeader": "Consent to Share Unencrypted Node Data via MQTT",
"consentText": "By enabling this feature, you acknowledge and expressly consent to the transmission of your device's real-time geographic location over the MQTT protocol without encryption. This location data may be used for purposes such as live map reporting, device tracking, and related telemetry functions."
} }
},
"mqttConfigCard": {
"label": "MQTT Config",
"description": "MQTT broker connection"
},
"mapReportingCard": {
"label": "Map reporting",
"description": "Periodic unencrypted map report"
} }
}, },
"neighborInfo": { "neighborInfo": {
@@ -307,6 +357,14 @@
"updateInterval": { "updateInterval": {
"label": "Update Interval", "label": "Update Interval",
"description": "Interval in seconds of how often we should try to send our Neighbor Info to the mesh" "description": "Interval in seconds of how often we should try to send our Neighbor Info to the mesh"
},
"neighborInfoConfig": {
"label": "Neighbor Info Config",
"description": "Neighbor Info configuration"
},
"transmitOverLora": {
"label": "Transmit over LoRa",
"description": "Whether NeighborInfo should be transmitted over LoRa. Not available on a channel with default key and name."
} }
}, },
"paxcounter": { "paxcounter": {
@@ -327,6 +385,10 @@
"bleThreshold": { "bleThreshold": {
"label": "BLE RSSI Threshold", "label": "BLE RSSI Threshold",
"description": "At what BLE RSSI level should the counter increase. Defaults to -80." "description": "At what BLE RSSI level should the counter increase. Defaults to -80."
},
"paxcounterConfig": {
"label": "Paxcounter Config",
"description": "Paxcounter configuration"
} }
}, },
"rangeTest": { "rangeTest": {
@@ -343,6 +405,10 @@
"save": { "save": {
"label": "Save CSV to storage", "label": "Save CSV to storage",
"description": "ESP32 Only" "description": "ESP32 Only"
},
"rangeTestConfig": {
"label": "Range Test Config",
"description": "Range Test configuration"
} }
}, },
"serial": { "serial": {
@@ -379,6 +445,10 @@
"overrideConsoleSerialPort": { "overrideConsoleSerialPort": {
"label": "Override Console Serial Port", "label": "Override Console Serial Port",
"description": "If you have a serial port connected to the console, this will override it." "description": "If you have a serial port connected to the console, this will override it."
},
"serialConfig": {
"label": "Serial Config",
"description": "Serial module configuration"
} }
}, },
"storeForward": { "storeForward": {
@@ -403,6 +473,14 @@
"historyReturnWindow": { "historyReturnWindow": {
"label": "History return window", "label": "History return window",
"description": "Return records from this time window (minutes)" "description": "Return records from this time window (minutes)"
},
"storeForwardConfig": {
"label": "Store & Forward Config",
"description": "Store & Forward configuration"
},
"isServer": {
"label": "Server",
"description": "Run as a Store & Forward server"
} }
}, },
"telemetry": { "telemetry": {
@@ -451,6 +529,14 @@
"airQualityScreenEnabled": { "airQualityScreenEnabled": {
"label": "Air Quality Screen Enabled", "label": "Air Quality Screen Enabled",
"description": "Show the Air Quality telemetry on the device screen" "description": "Show the Air Quality telemetry on the device screen"
},
"telemetryConfig": {
"label": "Telemetry Config",
"description": "Telemetry configuration"
},
"deviceTelemetryEnabled": {
"label": "Send Device Telemetry",
"description": "Enable/Disable the device telemetry module to send metrics to the mesh."
} }
}, },
"remoteHardware": { "remoteHardware": {
+44 -10
View File
@@ -54,28 +54,62 @@
"megahertz": "МГц", "megahertz": "МГц",
"kilohertz": "кГц", "kilohertz": "кГц",
"raw": "сырое", "raw": "сырое",
"meter": { "one": "метр", "plural": "метров", "suffix": "м" }, "meter": {
"kilometer": { "one": "километр", "plural": "километров", "suffix": "км" }, "one": "метр",
"minute": { "one": "минута", "plural": "минут" }, "plural": "метров",
"hour": { "one": "час", "plural": "часов" }, "suffix": "м"
},
"kilometer": {
"one": "километр",
"plural": "километров",
"suffix": "км"
},
"minute": {
"one": "минута",
"plural": "минут"
},
"hour": {
"one": "час",
"plural": "часов"
},
"millisecond": { "millisecond": {
"one": "миллисекунда", "one": "миллисекунда",
"plural": "миллисекунд", "plural": "миллисекунд",
"suffix": "мс" "suffix": "мс"
}, },
"second": { "one": "секунда", "plural": "секунд" }, "second": {
"one": "секунда",
"plural": "секунд"
},
"day": { "day": {
"one": "день", "one": "день",
"plural": "дней", "plural": "дней",
"today": "Сегодня", "today": "Сегодня",
"yesterday": "Вчера" "yesterday": "Вчера"
}, },
"month": { "one": "месяц", "plural": "месяцев" }, "month": {
"year": { "one": "год", "plural": "лет" }, "one": "месяц",
"plural": "месяцев"
},
"year": {
"one": "год",
"plural": "лет"
},
"snr": "SNR", "snr": "SNR",
"volt": { "one": "вольт", "plural": "вольт", "suffix": "В" }, "volt": {
"record": { "one": "запись", "plural": "записей" }, "one": "вольт",
"degree": { "one": "градус", "plural": "градусов", "suffix": "°" } "plural": "вольт",
"suffix": "В"
},
"record": {
"one": "запись",
"plural": "записей"
},
"degree": {
"one": "градус",
"plural": "градусов",
"suffix": "°"
}
}, },
"security": { "security": {
"0bit": "Пусто", "0bit": "Пусто",
@@ -29,6 +29,7 @@
"pickConnectionAgain": "Не удалось подключиться. Возможно, нужно заново выбрать устройство/порт.", "pickConnectionAgain": "Не удалось подключиться. Возможно, нужно заново выбрать устройство/порт.",
"added": "Подключение добавлено", "added": "Подключение добавлено",
"savedByName": "{{name}} сохранено.", "savedByName": "{{name}} сохранено.",
"savedCantConnect": "Подключение сохранено, но подключиться не удалось." "savedCantConnect": "Подключение сохранено, но подключиться не удалось.",
"checkConnetion": "Check your device or settings and try again"
} }
} }
+25 -7
View File
@@ -42,14 +42,28 @@
"commandPalette": "Поиск команд..." "commandPalette": "Поиск команд..."
}, },
"toast": { "toast": {
"positionRequestSent": { "title": "Запрос позиции отправлен." }, "positionRequestSent": {
"requestingPosition": { "title": "Запрашиваем позицию, пожалуйста, подождите..." }, "title": "Запрос позиции отправлен."
"sendingTraceroute": { "title": "Отправляем трассировку, пожалуйста, подождите..." }, },
"tracerouteSent": { "title": "Трассировка отправлена." }, "requestingPosition": {
"savedChannel": { "title": "Канал сохранён: {{channelName}}" }, "title": "Запрашиваем позицию, пожалуйста, подождите..."
},
"sendingTraceroute": {
"title": "Отправляем трассировку, пожалуйста, подождите..."
},
"tracerouteSent": {
"title": "Трассировка отправлена."
},
"savedChannel": {
"title": "Канал сохранён: {{channelName}}"
},
"messages": { "messages": {
"pkiEncryption": { "title": "Чат использует шифрование PKI." }, "pkiEncryption": {
"pskEncryption": { "title": "Чат использует шифрование PSK." } "title": "Чат использует шифрование PKI."
},
"pskEncryption": {
"title": "Чат использует шифрование PSK."
}
}, },
"configSaveError": { "configSaveError": {
"title": "Ошибка сохранения настроек", "title": "Ошибка сохранения настроек",
@@ -212,5 +226,9 @@
"footer": { "footer": {
"text": "Работает на <0>▲ Vercel</0> | Meshtastic® — зарегистрированный товарный знак Meshtastic LLC. | <1>Юридическая информация</1>", "text": "Работает на <0>▲ Vercel</0> | Meshtastic® — зарегистрированный товарный знак Meshtastic LLC. | <1>Юридическая информация</1>",
"commitSha": "SHA коммита: {{sha}}" "commitSha": "SHA коммита: {{sha}}"
},
"language": {
"label": "Язык",
"changeLanguage": "Change Language"
} }
} }
+14 -14
View File
@@ -1,16 +1,16 @@
{ {
"name": "Meshtastic", "name": "Meshtastic",
"short_name": "Web Client", "short_name": "Web Client",
"start_url": ".", "start_url": ".",
"description": "Meshtastic Web App", "description": "Meshtastic Web App",
"icons": [ "icons": [
{ {
"src": "/logo.svg", "src": "/logo.svg",
"sizes": "any", "sizes": "any",
"type": "image/svg+xml" "type": "image/svg+xml"
} }
], ],
"theme_color": "#67ea94", "theme_color": "#67ea94",
"background_color": "#67ea94", "background_color": "#67ea94",
"display": "standalone" "display": "standalone"
} }
+7 -7
View File
@@ -1,7 +1,9 @@
import { DeviceWrapper } from "@app/DeviceWrapper.tsx"; import { DeviceWrapper } from "@app/DeviceWrapper.tsx";
import { CommandPalette } from "@components/CommandPalette/index.tsx"; import { CommandPalette } from "@components/CommandPalette/index.tsx";
import { ConnectingOverlay } from "@components/ConnectingOverlay.tsx";
import { DialogManager } from "@components/Dialog/DialogManager.tsx"; import { DialogManager } from "@components/Dialog/DialogManager.tsx";
import { KeyBackupReminder } from "@components/KeyBackupReminder.tsx"; import { KeyBackupReminder } from "@components/KeyBackupReminder.tsx";
import { RegionSetupReminder } from "@components/RegionSetupReminder.tsx";
import { Toaster } from "@components/Toaster.tsx"; import { Toaster } from "@components/Toaster.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";
@@ -22,17 +24,14 @@ export function App() {
const device = getDevice(selectedDeviceId); const device = getDevice(selectedDeviceId);
return ( return (
// <ThemeProvider defaultTheme="system" storageKey="theme">
<ErrorBoundary FallbackComponent={ErrorPage}> <ErrorBoundary FallbackComponent={ErrorPage}>
{/* <NewDeviceDialog
open={connectDialogOpen}
onOpenChange={(open) => {
setConnectDialogOpen(open);
}}
/> */}
<Toaster /> <Toaster />
<TanStackRouterDevtools position="bottom-right" /> <TanStackRouterDevtools position="bottom-right" />
<DeviceWrapper deviceId={selectedDeviceId}> <DeviceWrapper deviceId={selectedDeviceId}>
{/* Overlay sits outside the device-conditional branch so it shows
during a first-time connect from the Connections screen as
well as reconnects from inside the app. */}
<ConnectingOverlay />
<div <div
className="flex h-screen flex-col bg-background-primary text-text-primary" className="flex h-screen flex-col bg-background-primary text-text-primary"
style={{ scrollbarWidth: "thin" }} style={{ scrollbarWidth: "thin" }}
@@ -43,6 +42,7 @@ export function App() {
<div className="h-full flex w-full"> <div className="h-full flex w-full">
<DialogManager /> <DialogManager />
<KeyBackupReminder /> <KeyBackupReminder />
<RegionSetupReminder />
<CommandPalette /> <CommandPalette />
<MapProvider> <MapProvider>
<Outlet /> <Outlet />
+3 -1
View File
@@ -8,6 +8,8 @@ export interface DeviceWrapperProps {
export const DeviceWrapper = ({ children, deviceId }: DeviceWrapperProps) => { export const DeviceWrapper = ({ children, deviceId }: DeviceWrapperProps) => {
return ( return (
<CurrentDeviceContext.Provider value={{ deviceId }}>{children}</CurrentDeviceContext.Provider> <CurrentDeviceContext.Provider value={{ deviceId }}>
{children}
</CurrentDeviceContext.Provider>
); );
}; };
@@ -1,7 +1,21 @@
import { vi } from "vitest"; import { vi } from "vitest";
vi.mock("@components/UI/Checkbox.tsx", () => ({ vi.mock("@components/UI/Checkbox.tsx", () => ({
Checkbox: ({ id, checked, onChange }: { id: string; checked: boolean; onChange: () => void }) => ( Checkbox: ({
<input data-testid="checkbox" type="checkbox" id={id} checked={checked} onChange={onChange} /> id,
checked,
onChange,
}: {
id: string;
checked: boolean;
onChange: () => void;
}) => (
<input
data-testid="checkbox"
type="checkbox"
id={id}
checked={checked}
onChange={onChange}
/>
), ),
})); }));
+10 -2
View File
@@ -1,4 +1,9 @@
import { BatteryFullIcon, BatteryLowIcon, BatteryMediumIcon, PlugZapIcon } from "lucide-react"; import {
BatteryFullIcon,
BatteryLowIcon,
BatteryMediumIcon,
PlugZapIcon,
} from "lucide-react";
import type React from "react"; import type React from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { DeviceMetrics } from "./types.ts"; import type { DeviceMetrics } from "./types.ts";
@@ -38,7 +43,10 @@ export const getBatteryStatus = (level: number): BatteryStatusKey => {
const BatteryStatus: React.FC<BatteryStatusProps> = ({ deviceMetrics }) => { const BatteryStatus: React.FC<BatteryStatusProps> = ({ deviceMetrics }) => {
const { t } = useTranslation(); const { t } = useTranslation();
if (deviceMetrics?.batteryLevel === undefined || deviceMetrics?.batteryLevel === null) { if (
deviceMetrics?.batteryLevel === undefined ||
deviceMetrics?.batteryLevel === null
) {
return null; return null;
} }
@@ -8,7 +8,8 @@ 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, useDevice, useDeviceStore, useNodeDB } from "@core/stores"; import { useNodesAsProto } from "@core/hooks/useNodesAsProto.ts";
import { useAppStore, useDevice, useDeviceStore } from "@core/stores";
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";
@@ -57,11 +58,16 @@ export interface SubItem {
} }
export const CommandPalette = () => { export const CommandPalette = () => {
const { commandPaletteOpen, setCommandPaletteOpen, setConnectDialogOpen, setSelectedDevice } = const {
useAppStore(); commandPaletteOpen,
setCommandPaletteOpen,
setConnectDialogOpen,
setSelectedDevice,
} = useAppStore();
const { getDevices } = useDeviceStore(); const { getDevices } = useDeviceStore();
const { setDialogOpen, connection } = useDevice(); const { setDialogOpen, connection } = useDevice();
const { getNode } = useNodeDB(); const allNodes = useNodesAsProto();
const getNode = (n: number) => allNodes.find((node) => node.num === n);
const { pinnedItems, togglePinnedItem } = usePinnedItems({ const { pinnedItems, togglePinnedItem } = usePinnedItems({
storageName: "pinnedCommandMenuGroups", storageName: "pinnedCommandMenuGroups",
}); });
@@ -114,7 +120,9 @@ export const CommandPalette = () => {
label: t("manage.command.switchNode"), label: t("manage.command.switchNode"),
icon: ArrowLeftRightIcon, icon: ArrowLeftRightIcon,
subItems: getDevices().map((device) => ({ subItems: getDevices().map((device) => ({
label: getNode(device.hardware.myNodeNum)?.user?.longName ?? t("unknown.shortName"), label:
getNode(device.hardware.myNodeNum)?.user?.longName ??
t("unknown.shortName"),
icon: <Avatar nodeNum={device.hardware.myNodeNum} />, icon: <Avatar nodeNum={device.hardware.myNodeNum} />,
action() { action() {
setSelectedDevice(device.id); setSelectedDevice(device.id);
@@ -257,7 +265,10 @@ export const CommandPalette = () => {
}, [setCommandPaletteOpen]); }, [setCommandPaletteOpen]);
return ( return (
<CommandDialog open={commandPaletteOpen} onOpenChange={setCommandPaletteOpen}> <CommandDialog
open={commandPaletteOpen}
onOpenChange={setCommandPaletteOpen}
>
<CommandInput placeholder={t("search.commandPalette")} /> <CommandInput placeholder={t("search.commandPalette")} />
<CommandList> <CommandList>
<CommandEmpty>{t("emptyState")}</CommandEmpty> <CommandEmpty>{t("emptyState")}</CommandEmpty>
@@ -0,0 +1,187 @@
import { Dialog, DialogContent, DialogTitle } from "@components/UI/Dialog.tsx";
import { Button } from "@components/UI/Button.tsx";
import { useDeviceStore } from "@core/stores";
import { cn } from "@core/utils/cn.ts";
import { useConnections } from "@app/pages/Connections/useConnections";
import { useConnectionProgress } from "@meshtastic/sdk-react";
import { Bluetooth, Cable, CheckCircle2, Globe, Loader2 } from "lucide-react";
import type { ComponentType, ReactElement } from "react";
import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
const STUCK_THRESHOLD_MS = 15_000;
const TRANSPORT_ICON: Record<
"http" | "bluetooth" | "serial",
ComponentType<{ className?: string }>
> = {
http: Globe,
bluetooth: Bluetooth,
serial: Cable,
};
/**
* Hero-card connect overlay. Visible while any saved connection's status
* is "connecting" or "configuring" — driven from the saved-connection
* status field rather than `device.connectionPhase` so the overlay shows
* during transport-open + storage-open (the device entry doesn't exist
* yet during those phases).
*
* Center: device name + transport icon with an animated radial pulse.
* Surrounding: 4 stat chips (Identity / Channels / Nodes / Metadata)
* that flip from gray to green as the SDK reports each piece arriving.
* Auto-dismisses on "configured" (with a brief success state) or
* "disconnected" / "error".
*/
export const ConnectingOverlay = (): ReactElement | null => {
const savedConnections = useDeviceStore((s) => s.savedConnections);
const progress = useConnectionProgress();
const { disconnect } = useConnections();
const { t } = useTranslation("connections");
const active = savedConnections.find(
(c) => c.status === "connecting" || c.status === "configuring",
);
// Stuck-detection: once an attempt has been visible for STUCK_THRESHOLD_MS,
// surface a Cancel button so the user can bail out of the overlay even when
// `onConfigComplete` never arrives (firmware in CLI / bootloader, framing
// out of sync, etc.). Resets when a fresh attempt starts.
const [showCancel, setShowCancel] = useState(false);
useEffect(() => {
if (!active) {
setShowCancel(false);
return;
}
setShowCancel(false);
const t = setTimeout(() => setShowCancel(true), STUCK_THRESHOLD_MS);
return () => clearTimeout(t);
}, [active?.id]);
if (!active) return null;
const Icon = TRANSPORT_ICON[active.type];
const counters =
progress.phase === "configuring" || progress.phase === "configured"
? progress.received
: {
config: 0,
modules: 0,
channels: 0,
nodes: 0,
myInfo: false,
metadata: false,
};
const isConnecting = active.status === "connecting";
const phaseLabel = isConnecting
? t("overlay.phase.connecting", { defaultValue: "Opening transport…" })
: t("overlay.phase.configuring", {
defaultValue: "Streaming configuration…",
});
return (
<Dialog open>
<DialogContent
className="max-w-sm overflow-hidden bg-background-primary p-0 text-text-primary shadow-2xl"
onInteractOutside={(e) => e.preventDefault()}
onEscapeKeyDown={(e) => e.preventDefault()}
>
<DialogTitle className="sr-only">
{t("overlay.srTitle", { defaultValue: "Connecting to device" })}
</DialogTitle>
<div className="flex flex-col items-center gap-6 px-8 pt-10 pb-8">
{/* Hero icon with radial pings — uses the theme link color so light
and dark modes both look at-home. */}
<div className="relative flex h-28 w-28 items-center justify-center">
<span className="absolute inset-0 animate-ping rounded-full bg-link/20 [animation-duration:1.8s]" />
<span className="absolute inset-3 animate-ping rounded-full bg-link/30 [animation-duration:2.4s] [animation-delay:0.4s]" />
<span className="absolute inset-6 rounded-full bg-link shadow-lg shadow-link/40" />
<Icon className="relative h-9 w-9 text-background-primary" />
</div>
{/* Name + phase */}
<div className="text-center">
<div className="text-lg font-semibold tracking-tight text-text-primary">
{active.name}
</div>
<div className="mt-1 inline-flex items-center gap-1.5 text-xs text-text-secondary">
<Loader2 className="h-3 w-3 animate-spin" />
{phaseLabel}
</div>
</div>
{/* Stat chips — 2x2 grid */}
<div className="grid w-full grid-cols-2 gap-2">
<Chip
label={t("overlay.chip.identity", { defaultValue: "Identity" })}
done={counters.myInfo}
/>
<Chip
label={t("overlay.chip.metadata", { defaultValue: "Metadata" })}
done={counters.metadata}
/>
<Chip
label={t("overlay.chip.channels", { defaultValue: "Channels" })}
count={counters.channels}
done={counters.channels > 0 && progress.phase === "configured"}
/>
<Chip
label={t("overlay.chip.nodes", { defaultValue: "Nodes" })}
count={counters.nodes}
done={counters.nodes > 0 && progress.phase === "configured"}
/>
</div>
{showCancel && (
<div className="flex w-full flex-col items-center gap-2">
<p className="text-center text-xs text-text-secondary">
{t("overlay.stuckHint", {
defaultValue:
"Taking longer than usual. The device may be in CLI / bootloader mode, or the firmware isn't responding to config requests.",
})}
</p>
<Button
type="button"
variant="outline"
onClick={() => void disconnect(active.id)}
>
{t("overlay.cancel", { defaultValue: "Cancel" })}
</Button>
</div>
)}
</div>
</DialogContent>
</Dialog>
);
};
interface ChipProps {
label: string;
count?: number;
done: boolean;
}
function Chip({ label, count, done }: ChipProps): ReactElement {
const filled = count !== undefined ? count > 0 : done;
return (
<div
className={cn(
"flex items-center justify-between gap-2 rounded-lg border px-3 py-2 text-sm transition-colors",
filled
? "border-green-500/40 bg-green-500/10 text-green-700 dark:text-green-300"
: "border-text-secondary/30 bg-text-secondary/5 text-text-secondary",
)}
>
<span className="font-medium">{label}</span>
{count !== undefined ? (
<span className="font-mono text-xs tabular-nums">{count}</span>
) : done ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
<span className="h-4 w-4 rounded-full border border-dashed border-text-secondary/50" />
)}
</div>
);
}
+17 -7
View File
@@ -1,6 +1,6 @@
import type { ConnectionStatus } from "@app/core/stores/deviceStore/types.ts"; import type { ConnectionStatus } from "@app/core/stores/deviceStore/types.ts";
import { cn } from "@core/utils/cn.ts"; import { cn } from "@core/utils/cn.ts";
import type { Protobuf } from "@meshtastic/core"; import type { Protobuf } from "@meshtastic/sdk";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { import {
ChevronRight, ChevronRight,
@@ -107,7 +107,9 @@ export const DeviceInfoPanel = ({
label: t("batteryVoltage.title"), label: t("batteryVoltage.title"),
icon: ZapIcon, icon: ZapIcon,
value: value:
voltage !== undefined ? `${voltage?.toPrecision(3)} V` : t("unknown.notAvailable", "N/A"), voltage !== undefined
? `${voltage?.toPrecision(3)} V`
: t("unknown.notAvailable", "N/A"),
}, },
{ {
id: "firmware", id: "firmware",
@@ -205,7 +207,9 @@ export const DeviceInfoPanel = ({
</button> </button>
)} )}
{!isCollapsed && <div className="my-2 h-px bg-gray-200 dark:bg-gray-700 flex-shrink-0" />} {!isCollapsed && (
<div className="my-2 h-px bg-gray-200 dark:bg-gray-700 flex-shrink-0" />
)}
<div <div
className={cn( className={cn(
@@ -237,7 +241,9 @@ export const DeviceInfoPanel = ({
})} })}
</div> </div>
{!isCollapsed && <div className="my-2 h-px bg-gray-200 dark:bg-gray-700 flex-shrink-0" />} {!isCollapsed && (
<div className="my-2 h-px bg-gray-200 dark:bg-gray-700 flex-shrink-0" />
)}
<div <div
className={cn( className={cn(
@@ -251,7 +257,9 @@ export const DeviceInfoPanel = ({
{actionButtons.map((buttonItem) => { {actionButtons.map((buttonItem) => {
const Icon = buttonItem.icon; const Icon = buttonItem.icon;
if (buttonItem.render) { if (buttonItem.render) {
return <Fragment key={buttonItem.id}>{buttonItem.render()}</Fragment>; return (
<Fragment key={buttonItem.id}>{buttonItem.render()}</Fragment>
);
} }
return ( return (
<Button <Button
@@ -273,7 +281,8 @@ export const DeviceInfoPanel = ({
"flex-shrink-0 w-4", "flex-shrink-0 w-4",
"text-gray-500 dark:text-gray-400", "text-gray-500 dark:text-gray-400",
"transition-colors duration-150", "transition-colors duration-150",
!disableHover && "group-hover:text-gray-700 dark:group-hover:text-gray-200", !disableHover &&
"group-hover:text-gray-700 dark:group-hover:text-gray-200",
)} )}
/> />
<Subtle <Subtle
@@ -281,7 +290,8 @@ export const DeviceInfoPanel = ({
"text-sm", "text-sm",
"text-gray-600 dark:text-gray-300", "text-gray-600 dark:text-gray-300",
"transition-colors duration-150", "transition-colors duration-150",
!disableHover && "group-hover:text-gray-800 dark:group-hover:text-gray-100", !disableHover &&
"group-hover:text-gray-800 dark:group-hover:text-gray-100",
)} )}
> >
{buttonItem.label} {buttonItem.label}
@@ -0,0 +1,53 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import AddConnectionDialog from "./AddConnectionDialog.tsx";
vi.mock("@core/hooks/useToast.ts", () => ({
useToast: () => ({ toast: vi.fn() }),
}));
vi.mock("@app/pages/Connections/utils", () => ({
testHttpReachable: vi.fn(() => Promise.resolve(true)),
}));
const getSwitch = () => screen.getByRole("switch");
describe("AddConnectionDialog HTTPS toggle", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("toggles between http and https when the app is served over http", () => {
render(<AddConnectionDialog open={true} isHTTPS={false} />);
const sw = getSwitch();
expect(sw).toHaveAttribute("aria-checked", "false");
expect(sw).toBeEnabled();
fireEvent.click(sw);
expect(sw).toHaveAttribute("aria-checked", "true");
fireEvent.click(sw);
expect(sw).toHaveAttribute("aria-checked", "false");
});
it("allows switching to http even when the app itself is served over https", () => {
render(<AddConnectionDialog open={true} isHTTPS={true} />);
const sw = getSwitch();
expect(sw).toHaveAttribute("aria-checked", "true");
expect(sw).toBeEnabled();
fireEvent.click(sw);
expect(sw).toHaveAttribute("aria-checked", "false");
});
it("keeps the protocol in sync with the url input prefix", () => {
render(<AddConnectionDialog open={true} isHTTPS={false} />);
// prefix reflects the current protocol
expect(screen.getByText("http://")).toBeInTheDocument();
fireEvent.click(getSwitch());
expect(screen.getByText("https://")).toBeInTheDocument();
});
});
@@ -1,11 +1,19 @@
import { SupportBadge } from "@app/components/Badge/SupportedBadge.tsx"; import { SupportBadge } from "@app/components/Badge/SupportedBadge.tsx";
import { Switch } from "@app/components/UI/Switch.tsx"; import { Switch } from "@app/components/UI/Switch.tsx";
import type { ConnectionType, NewConnection } from "@app/core/stores/deviceStore/types.ts"; import type {
ConnectionType,
NewConnection,
} from "@app/core/stores/deviceStore/types.ts";
import { testHttpReachable } from "@app/pages/Connections/utils"; import { testHttpReachable } from "@app/pages/Connections/utils";
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 { Label } from "@components/UI/Label.tsx"; import { Label } from "@components/UI/Label.tsx";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/UI/Tabs.tsx"; import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@components/UI/Tabs.tsx";
import { Link } from "@components/UI/Typography/Link.tsx"; import { Link } from "@components/UI/Typography/Link.tsx";
import { import {
type BrowserFeature, type BrowserFeature,
@@ -37,7 +45,9 @@ type DialogState = {
protocol: "http" | "https"; protocol: "http" | "https";
url: string; url: string;
testStatus: TestingStatus; testStatus: TestingStatus;
btSelected: { id: string; name?: string; device?: BluetoothDevice } | undefined; btSelected:
| { id: string; name?: string; device?: BluetoothDevice }
| undefined;
serialSelected: { vendorId?: number; productId?: number } | undefined; serialSelected: { vendorId?: number; productId?: number } | undefined;
}; };
@@ -50,7 +60,9 @@ type DialogAction =
| { type: "SET_TEST_STATUS"; payload: TestingStatus } | { type: "SET_TEST_STATUS"; payload: TestingStatus }
| { | {
type: "SET_BT_SELECTED"; type: "SET_BT_SELECTED";
payload: { id: string; name?: string; device?: BluetoothDevice } | undefined; payload:
| { id: string; name?: string; device?: BluetoothDevice }
| undefined;
} }
| { | {
type: "SET_SERIAL_SELECTED"; type: "SET_SERIAL_SELECTED";
@@ -70,27 +82,30 @@ type Pane = {
build: () => NewConnection | null; build: () => NewConnection | null;
}; };
const featureErrors: Record<BrowserFeature, { href: string; i18nKey: string }> = { const featureErrors: Record<BrowserFeature, { href: string; i18nKey: string }> =
"Web Bluetooth": { {
href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility", "Web Bluetooth": {
i18nKey: "addConnection.validation.requiresWebBluetooth", href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility",
}, i18nKey: "addConnection.validation.requiresWebBluetooth",
"Web Serial": { },
href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility", "Web Serial": {
i18nKey: "addConnection.validation.requiresWebSerial", href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility",
}, i18nKey: "addConnection.validation.requiresWebSerial",
"Secure Context": { },
href: "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts", "Secure Context": {
i18nKey: "addConnection.validation.requiresSecureContext", href: "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts",
}, i18nKey: "addConnection.validation.requiresSecureContext",
}; },
};
const FeatureErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => { const FeatureErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => {
if (missingFeatures.length === 0) { if (missingFeatures.length === 0) {
return null; return null;
} }
const browserFeatures = missingFeatures.filter((feature) => feature !== "Secure Context"); const browserFeatures = missingFeatures.filter(
(feature) => feature !== "Secure Context",
);
const needsSecureContext = missingFeatures.includes("Secure Context"); const needsSecureContext = missingFeatures.includes("Secure Context");
const needsFeature = const needsFeature =
@@ -100,6 +115,10 @@ const FeatureErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => {
? "Web Serial" ? "Web Serial"
: undefined; : undefined;
if (!needsFeature && !needsSecureContext) {
return null;
}
return ( return (
<div className="flex flex-col items-start gap-2 bg-red-500 p-4 rounded-md text-sm mt-4"> <div className="flex flex-col items-start gap-2 bg-red-500 p-4 rounded-md text-sm mt-4">
<div className="flex items-center gap-2 w-full"> <div className="flex items-center gap-2 w-full">
@@ -152,17 +171,22 @@ const initialState: DialogState = {
btSelected: undefined, btSelected: undefined,
serialSelected: undefined, serialSelected: undefined,
}; };
export const createInitialDialogState = (overrides?: Partial<DialogState>): DialogState => { export const createInitialDialogState = (
overrides?: Partial<DialogState>,
): DialogState => {
return { ...initialState, ...(overrides ?? {}) }; return { ...initialState, ...(overrides ?? {}) };
}; };
export const dialogStateInitializer = (overrides?: Partial<DialogState>): DialogState => export const dialogStateInitializer = (
createInitialDialogState(overrides); overrides?: Partial<DialogState>,
): DialogState => createInitialDialogState(overrides);
function dialogReducer(state: DialogState, action: DialogAction): DialogState { function dialogReducer(state: DialogState, action: DialogAction): DialogState {
switch (action.type) { switch (action.type) {
case "RESET": case "RESET":
return createInitialDialogState(action.payload?.isHTTPS ? { protocol: "https" } : {}); return createInitialDialogState(
action.payload?.isHTTPS ? { protocol: "https" } : {},
);
case "SET_TAB": case "SET_TAB":
return { ...state, tab: action.payload }; return { ...state, tab: action.payload };
case "SET_NAME": case "SET_NAME":
@@ -203,13 +227,22 @@ function PickerRow({
<div className="grid gap-2"> <div className="grid gap-2">
<Label>{label}</Label> <Label>{label}</Label>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button variant="subtle" className="gap-2" onClick={onPick} disabled={disabled}> <Button
variant="subtle"
className="gap-2"
onClick={onPick}
disabled={disabled}
>
<MousePointerClick className="h-4 w-4" /> <MousePointerClick className="h-4 w-4" />
{buttonText} {buttonText}
</Button> </Button>
<div className="text-sm text-slate-500 dark:text-slate-400 truncate">{display}</div> <div className="text-sm text-slate-500 dark:text-slate-400 truncate">
{display}
</div>
</div> </div>
{helper ? <p className="text-xs text-slate-500 dark:text-slate-400">{helper}</p> : null} {helper ? (
<p className="text-xs text-slate-500 dark:text-slate-400">{helper}</p>
) : null}
</div> </div>
); );
} }
@@ -238,9 +271,10 @@ export default function AddConnectionDialog({
const { unsupported } = useBrowserFeatureDetection(); const { unsupported } = useBrowserFeatureDetection();
const { t } = useTranslation(); const { t } = useTranslation();
const bluetoothSupported = typeof navigator !== "undefined" && "bluetooth" in navigator; const bluetoothSupported =
const serialSupported = typeof navigator !== "undefined" && "serial" in navigator; typeof navigator !== "undefined" && "bluetooth" in navigator;
const isURLHTTPS = isHTTPS; const serialSupported =
typeof navigator !== "undefined" && "serial" in navigator;
const reset = useCallback(() => { const reset = useCallback(() => {
dispatch({ type: "RESET", payload: { isHTTPS } }); dispatch({ type: "RESET", payload: { isHTTPS } });
@@ -268,7 +302,9 @@ export default function AddConnectionDialog({
if (!bluetoothSupported) { if (!bluetoothSupported) {
toast({ toast({
title: t("addConnection.bluetoothConnection.notSupported.title"), title: t("addConnection.bluetoothConnection.notSupported.title"),
description: t("addConnection.bluetoothConnection.notSupported.description"), description: t(
"addConnection.bluetoothConnection.notSupported.description",
),
}); });
return; return;
} }
@@ -303,7 +339,9 @@ export default function AddConnectionDialog({
if (!serialSupported) { if (!serialSupported) {
toast({ toast({
title: t("addConnection.serialConnection.notSupported.title"), title: t("addConnection.serialConnection.notSupported.title"),
description: t("addConnection.serialConnection.notSupported.description"), description: t(
"addConnection.serialConnection.notSupported.description",
),
}); });
return; return;
} }
@@ -335,7 +373,9 @@ export default function AddConnectionDialog({
} }
toast({ toast({
title: t("addConnection.serialConnection.portSelected.title"), title: t("addConnection.serialConnection.portSelected.title"),
description: t("addConnection.serialConnection.portSelected.description"), description: t(
"addConnection.serialConnection.portSelected.description",
),
}); });
} catch (err) { } catch (err) {
makeToastErrorHandler("Serial")(err); makeToastErrorHandler("Serial")(err);
@@ -360,7 +400,9 @@ export default function AddConnectionDialog({
dispatch({ type: "SET_TEST_STATUS", payload: "failure" }); dispatch({ type: "SET_TEST_STATUS", payload: "failure" });
toast({ toast({
title: t("addConnection.httpConnection.connectionTest.failure.title"), title: t("addConnection.httpConnection.connectionTest.failure.title"),
description: t("addConnection.httpConnection.connectionTest.failure.description"), description: t(
"addConnection.httpConnection.connectionTest.failure.description",
),
}); });
} }
}, [state.protocol, state.url, toast, t]); }, [state.protocol, state.url, toast, t]);
@@ -371,7 +413,9 @@ export default function AddConnectionDialog({
placeholder: t("addConnection.httpConnection.namePlaceholder"), placeholder: t("addConnection.httpConnection.namePlaceholder"),
children: () => ( children: () => (
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
<Label htmlFor="url">{t("addConnection.httpConnection.heading")}</Label> <Label htmlFor="url">
{t("addConnection.httpConnection.heading")}
</Label>
<Input <Input
id={"url"} id={"url"}
@@ -389,7 +433,6 @@ export default function AddConnectionDialog({
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-2 mt-1">
<Switch <Switch
value={state.protocol} value={state.protocol}
disabled={!!isURLHTTPS}
checked={state.protocol === "https"} checked={state.protocol === "https"}
onCheckedChange={(value) => { onCheckedChange={(value) => {
dispatch({ dispatch({
@@ -401,25 +444,37 @@ export default function AddConnectionDialog({
></Switch> ></Switch>
<Label>{t("addConnection.httpConnection.useHttps")}</Label> <Label>{t("addConnection.httpConnection.useHttps")}</Label>
</div> </div>
{isHTTPS && state.protocol === "http" && (
<p className="text-xs text-amber-600 dark:text-amber-400">
{t(
"addConnection.httpConnection.mixedContentWarning",
"This page is loaded over HTTPS. Your browser may block connections to an HTTP node.",
)}
</p>
)}
<div className="flex items-center gap-2 mt-4"> <div className="flex items-center gap-2 mt-4">
<Button <Button
variant="subtle" variant="subtle"
className="gap-2" className="gap-2"
onClick={handleTestHttp} onClick={handleTestHttp}
disabled={ disabled={
urlOrIpv4Schema.safeParse(`${state.protocol}://${state.url}`).success === false || urlOrIpv4Schema.safeParse(`${state.protocol}://${state.url}`)
state.testStatus === "testing" .success === false || state.testStatus === "testing"
} }
> >
{state.testStatus === "testing" ? ( {state.testStatus === "testing" ? (
<> <>
<Loader2 className="h-4 w-4 animate-spin" /> <Loader2 className="h-4 w-4 animate-spin" />
{t("addConnection.httpConnection.connectionTest.button.loading")} {t(
"addConnection.httpConnection.connectionTest.button.loading",
)}
</> </>
) : ( ) : (
<> <>
<MousePointerClick className="h-4 w-4" /> <MousePointerClick className="h-4 w-4" />
{t("addConnection.httpConnection.connectionTest.button.label")} {t(
"addConnection.httpConnection.connectionTest.button.label",
)}
</> </>
)} )}
</Button> </Button>
@@ -432,7 +487,9 @@ export default function AddConnectionDialog({
{state.testStatus === "failure" && ( {state.testStatus === "failure" && (
<div className="flex items-center gap-1 text-sm text-red-600 dark:text-red-400"> <div className="flex items-center gap-1 text-sm text-red-600 dark:text-red-400">
<XCircle className="h-4 w-4" /> <XCircle className="h-4 w-4" />
{t("addConnection.httpConnection.connectionTest.notReachable")} {t(
"addConnection.httpConnection.connectionTest.notReachable",
)}
</div> </div>
)} )}
</div> </div>
@@ -442,8 +499,8 @@ export default function AddConnectionDialog({
</div> </div>
), ),
validate: () => validate: () =>
urlOrIpv4Schema.safeParse(`${state.protocol}://${state.url}`).success === true && urlOrIpv4Schema.safeParse(`${state.protocol}://${state.url}`)
state.testStatus === "success", .success === true && state.testStatus === "success",
build: () => ({ build: () => ({
type: "http", type: "http",
name: state.name.trim(), name: state.name.trim(),
@@ -456,8 +513,12 @@ export default function AddConnectionDialog({
<> <>
<SupportBadge <SupportBadge
supported={bluetoothSupported} supported={bluetoothSupported}
labelSupported={t("addConnection.bluetoothConnection.supported.title")} labelSupported={t(
labelUnsupported={t("addConnection.bluetoothConnection.notSupported.title")} "addConnection.bluetoothConnection.supported.title",
)}
labelUnsupported={t(
"addConnection.bluetoothConnection.notSupported.title",
)}
/> />
<PickerRow <PickerRow
label={t("addConnection.bluetoothConnection.device")} label={t("addConnection.bluetoothConnection.device")}
@@ -471,7 +532,10 @@ export default function AddConnectionDialog({
} }
helper={t("addConnection.bluetoothConnection.helperText")} helper={t("addConnection.bluetoothConnection.helperText")}
/> />
<FeatureErrorMessage missingFeatures={unsupported} tabId="bluetooth" /> <FeatureErrorMessage
missingFeatures={unsupported}
tabId="bluetooth"
/>
</> </>
), ),
validate: () => state.name.trim().length > 0 && !!state.btSelected, validate: () => state.name.trim().length > 0 && !!state.btSelected,
@@ -489,8 +553,12 @@ export default function AddConnectionDialog({
<> <>
<SupportBadge <SupportBadge
supported={serialSupported} supported={serialSupported}
labelSupported={t("addConnection.serialConnection.supported.title")} labelSupported={t(
labelUnsupported={t("addConnection.serialConnection.notSupported.title")} "addConnection.serialConnection.supported.title",
)}
labelUnsupported={t(
"addConnection.serialConnection.notSupported.title",
)}
/> />
<PickerRow <PickerRow
label={t("addConnection.serialConnection.port")} label={t("addConnection.serialConnection.port")}
@@ -500,8 +568,10 @@ export default function AddConnectionDialog({
display={ display={
state.serialSelected state.serialSelected
? t("addConnection.serialConnection.deviceName", { ? t("addConnection.serialConnection.deviceName", {
vendorId: state.serialSelected.vendorId?.toString(16) ?? "?", vendorId:
productId: state.serialSelected.productId?.toString(16) ?? "?", state.serialSelected.vendorId?.toString(16) ?? "?",
productId:
state.serialSelected.productId?.toString(16) ?? "?",
}) })
: t("addConnection.serialConnection.notSelected") : t("addConnection.serialConnection.notSelected")
} }
@@ -511,7 +581,8 @@ export default function AddConnectionDialog({
</> </>
), ),
validate: () => validate: () =>
state.name.trim().length > 0 && (!!state.serialSelected || !serialSupported), state.name.trim().length > 0 &&
(!!state.serialSelected || !serialSupported),
build: () => ({ build: () => ({
type: "serial", type: "serial",
name: state.name.trim(), name: state.name.trim(),
@@ -524,7 +595,7 @@ export default function AddConnectionDialog({
state, state,
bluetoothSupported, bluetoothSupported,
serialSupported, serialSupported,
isURLHTTPS, isHTTPS,
handlePickBluetooth, handlePickBluetooth,
handlePickSerial, handlePickSerial,
handleTestHttp, handleTestHttp,
@@ -537,7 +608,8 @@ export default function AddConnectionDialog({
const canCreate = useMemo(() => currentPane.validate(), [currentPane]); const canCreate = useMemo(() => currentPane.validate(), [currentPane]);
const submit = const submit =
(fn: (p: NewConnection, device?: BluetoothDevice) => Promise<void>) => async () => { (fn: (p: NewConnection, device?: BluetoothDevice) => Promise<void>) =>
async () => {
if (!canCreate) { if (!canCreate) {
return; return;
} }
@@ -546,7 +618,8 @@ export default function AddConnectionDialog({
if (!payload) { if (!payload) {
return; return;
} }
const btDevice = state.tab === "bluetooth" ? state.btSelected?.device : undefined; const btDevice =
state.tab === "bluetooth" ? state.btSelected?.device : undefined;
await fn(payload, btDevice); await fn(payload, btDevice);
}; };
@@ -562,7 +635,9 @@ export default function AddConnectionDialog({
> >
<Tabs <Tabs
value={state.tab} value={state.tab}
onValueChange={(v) => dispatch({ type: "SET_TAB", payload: v as TabKey })} onValueChange={(v) =>
dispatch({ type: "SET_TAB", payload: v as TabKey })
}
> >
<TabsList className="grid grid-cols-3"> <TabsList className="grid grid-cols-3">
{TAB_META.map(({ key, label, Icon }) => ( {TAB_META.map(({ key, label, Icon }) => (
@@ -582,14 +657,20 @@ export default function AddConnectionDialog({
<Input <Input
id={`name-${state.tab}`} id={`name-${state.tab}`}
value={state.name} value={state.name}
onChange={(evt) => dispatch({ type: "SET_NAME", payload: evt.target.value })} onChange={(evt) =>
dispatch({ type: "SET_NAME", payload: evt.target.value })
}
placeholder={currentPane.placeholder} placeholder={currentPane.placeholder}
/> />
</div> </div>
{PANES[key].children()} {PANES[key].children()}
<div className="flex justify-end"> <div className="flex justify-end">
<div className="inline-flex rounded-md shadow-sm overflow-hidden border"> <div className="inline-flex rounded-md shadow-sm overflow-hidden border">
<Button onClick={submit(onSave)} disabled={!canCreate} className="rounded-none"> <Button
onClick={submit(onSave)}
disabled={!canCreate}
className="rounded-none"
>
{t("button.saveConnection")} {t("button.saveConnection")}
</Button> </Button>
</div> </div>
@@ -28,13 +28,18 @@ export const urlOrIpv4Schema = z
} }
// IPv4 pattern // IPv4 pattern
const ipv4Regex = /^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/; const ipv4Regex =
/^(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}$/;
// Domain pattern (e.g. example.com, meshtastic.local) // Domain pattern (e.g. example.com, meshtastic.local)
const domainRegex = /^(?!-)(?:[a-zA-Z0-9-]{1,63}\.)+[a-zA-Z]{2,}$/; const domainRegex = /^(?!-)(?:[a-zA-Z0-9-]{1,63}\.)+[a-zA-Z]{2,}$/;
// Local domain (e.g. meshtastic.local) // Local domain (e.g. meshtastic.local)
const localDomainRegex = /^(?!-)[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.local$/; const localDomainRegex = /^(?!-)[a-zA-Z0-9-]+(\.[a-zA-Z0-9-]+)*\.local$/;
return ipv4Regex.test(host) || domainRegex.test(host) || localDomainRegex.test(host); return (
ipv4Regex.test(host) ||
domainRegex.test(host) ||
localDomainRegex.test(host)
);
}, "Must be a valid IPv4 address or domain name with optional port (10-65535)") }, "Must be a valid IPv4 address or domain name with optional port (10-65535)")
.transform((val) => { .transform((val) => {
return /^https?:\/\//i.test(val) ? val : `http://${val}`; return /^https?:\/\//i.test(val) ? val : `http://${val}`;
@@ -47,7 +47,9 @@ describe("ClearAllStoresDialog", () => {
it("calls clearAllStores and navigates to '/' when confirm is clicked", () => { it("calls clearAllStores and navigates to '/' when confirm is clicked", () => {
render(<ClearAllStoresDialog open onOpenChange={mockOnOpenChange} />); render(<ClearAllStoresDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(screen.getByRole("button", { name: "Clear all local storage" })); fireEvent.click(
screen.getByRole("button", { name: "Clear all local storage" }),
);
expect(mockClearAllStores).toHaveBeenCalledTimes(1); expect(mockClearAllStores).toHaveBeenCalledTimes(1);
expect(assignedHref).toBe("/"); // forced reload target expect(assignedHref).toBe("/"); // forced reload target
@@ -7,7 +7,10 @@ export interface ClearAllStoresDialogProps {
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
} }
export const ClearAllStoresDialog = ({ open, onOpenChange }: ClearAllStoresDialogProps) => { export const ClearAllStoresDialog = ({
open,
onOpenChange,
}: ClearAllStoresDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const handleClearAllStores = () => { const handleClearAllStores = () => {
@@ -21,9 +21,13 @@ export interface ClientNotificationDialogProps {
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
} }
export const ClientNotificationDialog = ({ open, onOpenChange }: ClientNotificationDialogProps) => { export const ClientNotificationDialog = ({
open,
onOpenChange,
}: ClientNotificationDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const { connection, getClientNotification, removeClientNotification } = useDevice(); const { connection, getClientNotification, removeClientNotification } =
useDevice();
const [securityNumber, setSecurityNumber] = useState(""); const [securityNumber, setSecurityNumber] = useState("");
const notification = getClientNotification(0); const notification = getClientNotification(0);
@@ -42,9 +46,11 @@ export const ClientNotificationDialog = ({ open, onOpenChange }: ClientNotificat
nonce: bigint, nonce: bigint,
secNum?: number, secNum?: number,
) => { ) => {
connection?.sendKeyVerification(messageType, 0, nonce, secNum).catch((error) => { connection
console.error("Failed to send key verification message:", error); ?.sendKeyVerification(messageType, 0, nonce, secNum)
}); .catch((error) => {
console.error("Failed to send key verification message:", error);
});
dismiss(); dismiss();
}; };
@@ -62,7 +68,9 @@ export const ClientNotificationDialog = ({ open, onOpenChange }: ClientNotificat
<DialogHeader> <DialogHeader>
<DialogTitle>{t("keyVerification.inform.title")}</DialogTitle> <DialogTitle>{t("keyVerification.inform.title")}</DialogTitle>
<DialogDescription> <DialogDescription>
{t("keyVerification.inform.description", { name: value.remoteLongname })} {t("keyVerification.inform.description", {
name: value.remoteLongname,
})}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<p className="py-4 text-center font-mono text-4xl tracking-widest"> <p className="py-4 text-center font-mono text-4xl tracking-widest">
@@ -84,7 +92,9 @@ export const ClientNotificationDialog = ({ open, onOpenChange }: ClientNotificat
<DialogHeader> <DialogHeader>
<DialogTitle>{t("keyVerification.request.title")}</DialogTitle> <DialogTitle>{t("keyVerification.request.title")}</DialogTitle>
<DialogDescription> <DialogDescription>
{t("keyVerification.request.description", { name: value.remoteLongname })} {t("keyVerification.request.description", {
name: value.remoteLongname,
})}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<Input <Input
@@ -102,7 +112,11 @@ export const ClientNotificationDialog = ({ open, onOpenChange }: ClientNotificat
variant="default" variant="default"
disabled={securityNumber === ""} disabled={securityNumber === ""}
onClick={() => onClick={() =>
respond(MessageType.PROVIDE_SECURITY_NUMBER, value.nonce, Number(securityNumber)) respond(
MessageType.PROVIDE_SECURITY_NUMBER,
value.nonce,
Number(securityNumber),
)
} }
> >
{t("keyVerification.request.submit")} {t("keyVerification.request.submit")}
@@ -119,7 +133,9 @@ export const ClientNotificationDialog = ({ open, onOpenChange }: ClientNotificat
<DialogHeader> <DialogHeader>
<DialogTitle>{t("keyVerification.final.title")}</DialogTitle> <DialogTitle>{t("keyVerification.final.title")}</DialogTitle>
<DialogDescription> <DialogDescription>
{t("keyVerification.final.description", { name: value.remoteLongname })} {t("keyVerification.final.description", {
name: value.remoteLongname,
})}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<p className="py-4 text-center font-mono text-4xl tracking-widest"> <p className="py-4 text-center font-mono text-4xl tracking-widest">
@@ -132,7 +148,10 @@ export const ClientNotificationDialog = ({ open, onOpenChange }: ClientNotificat
> >
{t("keyVerification.final.reject")} {t("keyVerification.final.reject")}
</Button> </Button>
<Button variant="default" onClick={() => respond(MessageType.DO_VERIFY, value.nonce)}> <Button
variant="default"
onClick={() => respond(MessageType.DO_VERIFY, value.nonce)}
>
{t("keyVerification.final.verify")} {t("keyVerification.final.verify")}
</Button> </Button>
</DialogFooter> </DialogFooter>
@@ -145,7 +164,10 @@ export const ClientNotificationDialog = ({ open, onOpenChange }: ClientNotificat
<DialogHeader> <DialogHeader>
<DialogTitle>{t("clientNotification.title")}</DialogTitle> <DialogTitle>{t("clientNotification.title")}</DialogTitle>
<DialogDescription> <DialogDescription>
{t([`clientNotification.${notification?.message}`, notification?.message ?? ""])} {t([
`clientNotification.${notification?.message}`,
notification?.message ?? "",
])}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
); );
@@ -1,33 +1,25 @@
import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx"; import { DeleteMessagesDialog } from "@components/Dialog/DeleteMessagesDialog/DeleteMessagesDialog.tsx";
import { type MessageStore, useMessages } from "@core/stores"; import { useActiveClient } from "@meshtastic/sdk-react";
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";
vi.mock("@core/stores", () => ({ const mockClearAll = vi.fn();
CurrentDeviceContext: {
_currentValue: { deviceId: 1234 }, vi.mock("@meshtastic/sdk-react", () => ({
}, useActiveClient: vi.fn(() => ({
useMessages: vi.fn(() => ({ chat: { clearAll: mockClearAll },
deleteAllMessages: vi.fn(),
})), })),
})); }));
describe("DeleteMessagesDialog", () => { describe("DeleteMessagesDialog", () => {
const mockOnOpenChange = vi.fn(); const mockOnOpenChange = vi.fn();
const mockClearAllMessages = vi.fn();
beforeEach(() => { beforeEach(() => {
mockOnOpenChange.mockClear(); mockOnOpenChange.mockClear();
mockClearAllMessages.mockClear(); mockClearAll.mockClear();
vi.mocked(useActiveClient).mockReturnValue({
const mockedUseMessages = vi.mocked(useMessages); chat: { clearAll: mockClearAll },
mockedUseMessages.mockImplementation( } as never);
() =>
({
deleteAllMessages: mockClearAllMessages,
}) as unknown as MessageStore,
);
mockedUseMessages.mockClear();
}); });
it("calls onOpenChange with false when the close button (X) is clicked", () => { it("calls onOpenChange with false when the close button (X) is clicked", () => {
@@ -46,28 +38,42 @@ describe("DeleteMessagesDialog", () => {
it("renders the dialog when open is true", () => { it("renders the dialog when open is true", () => {
render(<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />); render(<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />);
expect(screen.getByText("Clear All Messages")).toBeInTheDocument(); expect(screen.getByText("Clear All Messages")).toBeInTheDocument();
expect(screen.getByText(/This action will clear all message history./)).toBeInTheDocument(); expect(
screen.getByText(/This action will clear all message history./),
).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Dismiss" })).toBeInTheDocument(); expect(screen.getByRole("button", { name: "Dismiss" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Clear Messages" })).toBeInTheDocument(); expect(
screen.getByRole("button", { name: "Clear Messages" }),
).toBeInTheDocument();
}); });
it("does not render the dialog when open is false", () => { it("does not render the dialog when open is false", () => {
render(<DeleteMessagesDialog open={false} onOpenChange={mockOnOpenChange} />); render(
<DeleteMessagesDialog open={false} onOpenChange={mockOnOpenChange} />,
);
expect(screen.queryByText("Clear All Messages")).toBeNull(); expect(screen.queryByText("Clear All Messages")).toBeNull();
}); });
it("calls onOpenChange with false when the dismiss button is clicked", () => { it("calls onOpenChange with false when the dismiss button is clicked", () => {
render(<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />); render(<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(screen.getByRole("button", { name: "Dismiss" })); fireEvent.click(screen.getByRole("button", { name: "Dismiss" }));
expect(mockOnOpenChange).toHaveBeenCalledTimes(1); // Add count check expect(mockOnOpenChange).toHaveBeenCalledTimes(1);
expect(mockOnOpenChange).toHaveBeenCalledWith(false); expect(mockOnOpenChange).toHaveBeenCalledWith(false);
}); });
it("calls deleteAllMessages and onOpenChange with false when the clear messages button is clicked", () => { it("calls chat.clearAll and onOpenChange with false when Clear Messages is clicked", () => {
render(<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />); render(<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(screen.getByRole("button", { name: "Clear Messages" })); fireEvent.click(screen.getByRole("button", { name: "Clear Messages" }));
expect(mockClearAllMessages).toHaveBeenCalledTimes(1); expect(mockClearAll).toHaveBeenCalledTimes(1);
expect(mockOnOpenChange).toHaveBeenCalledTimes(1); // Add count check expect(mockOnOpenChange).toHaveBeenCalledTimes(1);
expect(mockOnOpenChange).toHaveBeenCalledWith(false);
});
it("no-ops gracefully when there is no active client", () => {
vi.mocked(useActiveClient).mockReturnValue(undefined);
render(<DeleteMessagesDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(screen.getByRole("button", { name: "Clear Messages" }));
expect(mockClearAll).not.toHaveBeenCalled();
expect(mockOnOpenChange).toHaveBeenCalledWith(false); expect(mockOnOpenChange).toHaveBeenCalledWith(false);
}); });
}); });
@@ -1,4 +1,4 @@
import { useMessages } from "@app/core/stores/index.ts"; import { useActiveClient } from "@meshtastic/sdk-react";
import { AlertTriangleIcon } from "lucide-react"; import { AlertTriangleIcon } from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { DialogWrapper } from "../DialogWrapper.tsx"; import { DialogWrapper } from "../DialogWrapper.tsx";
@@ -8,12 +8,15 @@ export interface DeleteMessagesDialogProps {
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
} }
export const DeleteMessagesDialog = ({ open, onOpenChange }: DeleteMessagesDialogProps) => { export const DeleteMessagesDialog = ({
open,
onOpenChange,
}: DeleteMessagesDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const messageStore = useMessages(); const meshClient = useActiveClient();
const handleConfirm = () => { const handleConfirm = () => {
messageStore.deleteAllMessages(); void meshClient?.chat.clearAll();
onOpenChange(false); onOpenChange(false);
}; };
@@ -16,7 +16,7 @@ import { UnsafeRolesDialog } from "@components/Dialog/UnsafeRolesDialog/UnsafeRo
import { useDevice } from "@core/stores"; import { useDevice } from "@core/stores";
export const DialogManager = () => { export const DialogManager = () => {
const { channels, config, dialog, setDialogOpen } = useDevice(); const { config, dialog, setDialogOpen } = useDevice();
return ( return (
<> <>
<QRDialog <QRDialog
@@ -24,7 +24,6 @@ export const DialogManager = () => {
onOpenChange={(open) => { onOpenChange={(open) => {
setDialogOpen("QR", open); setDialogOpen("QR", open);
}} }}
channels={channels}
loraConfig={config.lora} loraConfig={config.lora}
/> />
<ImportDialog <ImportDialog
@@ -33,7 +33,9 @@ describe("FactoryResetConfigDialog", () => {
it("calls factoryResetConfig and then closes the dialog on confirm", async () => { it("calls factoryResetConfig and then closes the dialog on confirm", async () => {
render(<FactoryResetConfigDialog open onOpenChange={mockOnOpenChange} />); render(<FactoryResetConfigDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(screen.getByRole("button", { name: "Factory Reset Config" })); fireEvent.click(
screen.getByRole("button", { name: "Factory Reset Config" }),
);
expect(mockFactoryReset).toHaveBeenCalledTimes(1); expect(mockFactoryReset).toHaveBeenCalledTimes(1);
@@ -8,7 +8,10 @@ export interface FactoryResetConfigDialogProps {
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
} }
export const FactoryResetConfigDialog = ({ open, onOpenChange }: FactoryResetConfigDialogProps) => { export const FactoryResetConfigDialog = ({
open,
onOpenChange,
}: FactoryResetConfigDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const { connection } = useDevice(); const { connection } = useDevice();
@@ -4,21 +4,12 @@ import { FactoryResetDeviceDialog } from "./FactoryResetDeviceDialog.tsx";
const mockFactoryResetDevice = vi.fn(); const mockFactoryResetDevice = vi.fn();
const mockRemoveDevice = vi.fn(); const mockRemoveDevice = vi.fn();
const mockRemoveMessageStore = vi.fn();
const mockRemoveNodeDB = vi.fn();
const mockToast = vi.fn(); const mockToast = vi.fn();
vi.mock("@core/stores", () => { vi.mock("@core/stores", () => {
// Make each store a callable fn (like a Zustand hook), and attach .getState()
const useDeviceStore = Object.assign(vi.fn(), { const useDeviceStore = Object.assign(vi.fn(), {
getState: () => ({ removeDevice: mockRemoveDevice }), getState: () => ({ removeDevice: mockRemoveDevice }),
}); });
const useMessageStore = Object.assign(vi.fn(), {
getState: () => ({ removeMessageStore: mockRemoveMessageStore }),
});
const useNodeDBStore = Object.assign(vi.fn(), {
getState: () => ({ removeNodeDB: mockRemoveNodeDB }),
});
return { return {
CurrentDeviceContext: { CurrentDeviceContext: {
@@ -29,8 +20,6 @@ vi.mock("@core/stores", () => {
connection: { factoryResetDevice: mockFactoryResetDevice }, connection: { factoryResetDevice: mockFactoryResetDevice },
}), }),
useDeviceStore, useDeviceStore,
useMessageStore,
useNodeDBStore,
}; };
}); });
@@ -45,13 +34,10 @@ describe("FactoryResetDeviceDialog", () => {
mockOnOpenChange.mockClear(); mockOnOpenChange.mockClear();
mockFactoryResetDevice.mockReset(); mockFactoryResetDevice.mockReset();
mockRemoveDevice.mockClear(); mockRemoveDevice.mockClear();
mockRemoveMessageStore.mockClear();
mockRemoveNodeDB.mockClear();
mockToast.mockClear(); mockToast.mockClear();
}); });
it("calls factoryResetDevice, closes dialog, and after reset resolves clears messages and node DB", async () => { it("calls factoryResetDevice, closes dialog, and after reset resolves removes the device", async () => {
// Control the promise returned by factoryResetDevice
let resolveReset: (() => void) | undefined; let resolveReset: (() => void) | undefined;
mockFactoryResetDevice.mockImplementation( mockFactoryResetDevice.mockImplementation(
() => () =>
@@ -61,23 +47,20 @@ describe("FactoryResetDeviceDialog", () => {
); );
render(<FactoryResetDeviceDialog open onOpenChange={mockOnOpenChange} />); render(<FactoryResetDeviceDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(screen.getByRole("button", { name: "Factory Reset Device" })); fireEvent.click(
screen.getByRole("button", { name: "Factory Reset Device" }),
);
// Called immediately
expect(mockFactoryResetDevice).toHaveBeenCalledTimes(1); expect(mockFactoryResetDevice).toHaveBeenCalledTimes(1);
// DialogWrapper awaits onConfirm (which returns undefined), so close happens on next microtask
await waitFor(() => { await waitFor(() => {
expect(mockOnOpenChange).toHaveBeenCalledTimes(1); expect(mockOnOpenChange).toHaveBeenCalledTimes(1);
expect(mockOnOpenChange).toHaveBeenCalledWith(false); expect(mockOnOpenChange).toHaveBeenCalledWith(false);
}); });
// Resolve the reset
resolveReset?.(); resolveReset?.();
expect(mockRemoveDevice).toHaveBeenCalledTimes(1); expect(mockRemoveDevice).toHaveBeenCalledTimes(1);
expect(mockRemoveMessageStore).toHaveBeenCalledTimes(1);
expect(mockRemoveNodeDB).toHaveBeenCalledTimes(1);
}); });
it("calls onOpenChange(false) and does not call factoryResetDevice when cancel is clicked", async () => { it("calls onOpenChange(false) and does not call factoryResetDevice when cancel is clicked", async () => {
@@ -91,7 +74,5 @@ describe("FactoryResetDeviceDialog", () => {
expect(mockFactoryResetDevice).not.toHaveBeenCalled(); expect(mockFactoryResetDevice).not.toHaveBeenCalled();
expect(mockRemoveDevice).not.toHaveBeenCalled(); expect(mockRemoveDevice).not.toHaveBeenCalled();
expect(mockRemoveMessageStore).not.toHaveBeenCalled();
expect(mockRemoveNodeDB).not.toHaveBeenCalled();
}); });
}); });
@@ -1,5 +1,5 @@
import { toast } from "@core/hooks/useToast.ts"; import { toast } from "@core/hooks/useToast.ts";
import { useDevice, useDeviceStore, useMessageStore, useNodeDBStore } from "@core/stores"; import { useDevice, useDeviceStore } from "@core/stores";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { DialogWrapper } from "../DialogWrapper.tsx"; import { DialogWrapper } from "../DialogWrapper.tsx";
@@ -8,7 +8,10 @@ export interface FactoryResetDeviceDialogProps {
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
} }
export const FactoryResetDeviceDialog = ({ open, onOpenChange }: FactoryResetDeviceDialogProps) => { export const FactoryResetDeviceDialog = ({
open,
onOpenChange,
}: FactoryResetDeviceDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const { connection, id } = useDevice(); const { connection, id } = useDevice();
@@ -23,8 +26,6 @@ export const FactoryResetDeviceDialog = ({ open, onOpenChange }: FactoryResetDev
// The device will be wiped and disconnected without resolving the promise // The device will be wiped and disconnected without resolving the promise
// so we proceed to clear all data associated with the device immediately // so we proceed to clear all data associated with the device immediately
useDeviceStore.getState().removeDevice(id); useDeviceStore.getState().removeDevice(id);
useMessageStore.getState().removeMessageStore(id);
useNodeDBStore.getState().removeNodeDB(id);
// Reload the app to ensure all ephemeral state is cleared // Reload the app to ensure all ephemeral state is cleared
window.location.href = "/"; window.location.href = "/";
+51 -35
View File
@@ -20,8 +20,8 @@ import {
} from "@components/UI/Select.tsx"; } from "@components/UI/Select.tsx";
import { Switch } from "@components/UI/Switch.tsx"; import { Switch } from "@components/UI/Switch.tsx";
import { useDevice } from "@core/stores"; import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; import { Protobuf } from "@meshtastic/sdk";
import { Protobuf } from "@meshtastic/core"; import { useConfigEditor } from "@meshtastic/sdk-react";
import { toByteArray } from "base64-js"; import { toByteArray } from "base64-js";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
@@ -33,7 +33,8 @@ export interface ImportDialogProps {
} }
export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => { export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
const { setChange, channels, config } = useDevice(); const { config } = useDevice();
const editor = useConfigEditor();
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const [importDialogInput, setImportDialogInput] = useState<string>(""); const [importDialogInput, setImportDialogInput] = useState<string>("");
const [channelSet, setChannelSet] = useState<Protobuf.AppOnly.ChannelSet>(); const [channelSet, setChannelSet] = useState<Protobuf.AppOnly.ChannelSet>();
@@ -47,7 +48,8 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
try { try {
const channelsUrl = new URL(importDialogInput); const channelsUrl = new URL(importDialogInput);
if ( if (
(channelsUrl.hostname !== "meshtastic.org" && channelsUrl.pathname !== "/e/") || (channelsUrl.hostname !== "meshtastic.org" &&
channelsUrl.pathname !== "/e/") ||
!channelsUrl.hash !channelsUrl.hash
) { ) {
throw t("import.error.invalidUrl"); throw t("import.error.invalidUrl");
@@ -55,7 +57,11 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
const encodedChannelConfig = channelsUrl.hash.substring(1); const encodedChannelConfig = channelsUrl.hash.substring(1);
const paddedString = encodedChannelConfig const paddedString = encodedChannelConfig
.padEnd(encodedChannelConfig.length + ((4 - (encodedChannelConfig.length % 4)) % 4), "=") .padEnd(
encodedChannelConfig.length +
((4 - (encodedChannelConfig.length % 4)) % 4),
"=",
)
.replace(/-/g, "+") .replace(/-/g, "+")
.replace(/_/g, "/"); .replace(/_/g, "/");
@@ -77,38 +83,32 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
}, [importDialogInput, t]); }, [importDialogInput, t]);
const apply = () => { const apply = () => {
channelSet?.settings.forEach((ch: Protobuf.Channel.ChannelSettings, index: number) => { if (!editor) return;
if (importIndex[index] === -1) { channelSet?.settings.forEach(
return; (ch: Protobuf.Channel.ChannelSettings, index: number) => {
} if (importIndex[index] === -1) {
return;
}
const payload = create(Protobuf.Channel.ChannelSchema, { const payload = create(Protobuf.Channel.ChannelSchema, {
index: importIndex[index], index: importIndex[index],
role: role:
importIndex[index] === 0 importIndex[index] === 0
? Protobuf.Channel.Channel_Role.PRIMARY ? Protobuf.Channel.Channel_Role.PRIMARY
: Protobuf.Channel.Channel_Role.SECONDARY, : Protobuf.Channel.Channel_Role.SECONDARY,
settings: ch, settings: ch,
}); });
if (!deepCompareConfig(channels.get(importIndex[index] ?? 0), payload, true)) { editor.setChannel(payload);
setChange( },
{ type: "channel", index: importIndex[index] ?? 0 }, );
payload,
channels.get(importIndex[index] ?? 0),
);
}
});
if (channelSet?.loraConfig && updateConfig) { if (channelSet?.loraConfig && updateConfig) {
const payload = { const payload = {
...config.lora, ...config.lora,
...channelSet.loraConfig, ...channelSet.loraConfig,
}; } as Protobuf.Config.Config_LoRaConfig;
editor.setRadioSection("lora", payload);
if (!deepCompareConfig(config.lora, payload, true)) {
setChange({ type: "config", variant: "lora" }, payload, config.lora);
}
} }
// Reset state after import // Reset state after import
setImportDialogInput(""); setImportDialogInput("");
@@ -133,14 +133,23 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
<DialogHeader> <DialogHeader>
<DialogTitle>{t("import.title")}</DialogTitle> <DialogTitle>{t("import.title")}</DialogTitle>
<DialogDescription> <DialogDescription>
<Trans i18nKey={"import.description"} components={{ italic: <i />, br: <br /> }} /> <Trans
i18nKey={"import.description"}
components={{ italic: <i />, br: <br /> }}
/>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<Label>{t("import.channelSetUrl")}</Label> <Label>{t("import.channelSetUrl")}</Label>
<Input <Input
value={importDialogInput} value={importDialogInput}
variant={importDialogInput === "" ? "default" : validUrl ? "dirty" : "invalid"} variant={
importDialogInput === ""
? "default"
: validUrl
? "dirty"
: "invalid"
}
onChange={(e) => { onChange={(e) => {
setImportDialogInput(e.target.value); setImportDialogInput(e.target.value);
}} }}
@@ -169,7 +178,10 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
<span className="flex-1">{t("import.channelSlot")}</span> <span className="flex-1">{t("import.channelSlot")}</span>
</div> </div>
{channelSet?.settings.map((channel, index) => ( {channelSet?.settings.map((channel, index) => (
<div className="flex items-center" key={`channel_${channel.id}_${index}`}> <div
className="flex items-center"
key={`channel_${channel.id}_${index}`}
>
<Label className="flex-1"> <Label className="flex-1">
{channel.name.length {channel.name.length
? channel.name ? channel.name
@@ -189,10 +201,14 @@ export const ImportDialog = ({ open, onOpenChange }: ImportDialogProps) => {
disabled={importIndex.includes(i) && index !== i} disabled={importIndex.includes(i) && index !== i}
value={i.toString()} value={i.toString()}
> >
{i === 0 ? t("import.primary") : `${t("import.channelPrefix")}${i}`} {i === 0
? t("import.primary")
: `${t("import.channelPrefix")}${i}`}
</SelectItem> </SelectItem>
))} ))}
<SelectItem value="-1">{t("import.doNotImport")}</SelectItem> <SelectItem value="-1">
{t("import.doNotImport")}
</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@@ -1,6 +1,6 @@
import { useNodeDB } from "@core/stores"; import { useNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
import type { Protobuf, Types } from "@meshtastic/core"; import type { Protobuf, Types } from "@meshtastic/sdk";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { numberToHexUnpadded } from "@noble/curves/utils.js";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
Dialog, Dialog,
@@ -23,14 +23,15 @@ export const LocationResponseDialog = ({
onOpenChange, onOpenChange,
}: LocationResponseDialogProps) => { }: LocationResponseDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const { getNode } = useNodeDB(); const from = useNodeAsProto(location?.from ?? 0);
const from = getNode(location?.from ?? 0);
const longName = const longName =
from?.user?.longName ?? (from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName")); from?.user?.longName ??
(from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
const shortName = const shortName =
from?.user?.shortName ?? from?.user?.shortName ??
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : t("unknown.shortName")); (from
? `${numberToHexUnpadded(from?.num).substring(0, 4)}`
: t("unknown.shortName"));
const position = location?.data; const position = location?.data;
@@ -67,18 +68,23 @@ export const LocationResponseDialog = ({
rel="noreferrer" rel="noreferrer"
> >
{" "} {" "}
{position.latitudeI ?? 0 / 1e7}, {position.longitudeI ?? 0 / 1e7} {position.latitudeI ?? 0 / 1e7},{" "}
{position.longitudeI ?? 0 / 1e7}
</a> </a>
</p> </p>
<p> <p>
{t("locationResponse.altitude")} {position.altitude} {t("locationResponse.altitude")} {position.altitude}
{(position.altitude ?? 0) < 1 ? t("unit.meter.one") : t("unit.meter.plural")} {(position.altitude ?? 0) < 1
? t("unit.meter.one")
: t("unit.meter.plural")}
</p> </p>
</span> </span>
</div> </div>
) : ( ) : (
// Optional: Show a message if coordinates are not available // Optional: Show a message if coordinates are not available
<p className="text-textPrimary">{t("locationResponse.noCoordinates")}</p> <p className="text-textPrimary">
{t("locationResponse.noCoordinates")}
</p>
)} )}
</DialogDescription> </DialogDescription>
</DialogContent> </DialogContent>
@@ -18,7 +18,11 @@ export interface ManagedModeDialogProps {
onSubmit: () => void; onSubmit: () => void;
} }
export const ManagedModeDialog = ({ open, onOpenChange, onSubmit }: ManagedModeDialogProps) => { export const ManagedModeDialog = ({
open,
onOpenChange,
onSubmit,
}: ManagedModeDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const [confirmState, setConfirmState] = useState(false); const [confirmState, setConfirmState] = useState(false);
@@ -43,7 +47,9 @@ export const ManagedModeDialog = ({ open, onOpenChange, onSubmit }: ManagedModeD
onChange={() => setConfirmState(!confirmState)} onChange={() => setConfirmState(!confirmState)}
name="confirmUnderstanding" name="confirmUnderstanding"
> >
<p className="dark:text-white pt-1">{t("managedMode.confirmUnderstanding")}</p> <p className="dark:text-white pt-1">
{t("managedMode.confirmUnderstanding")}
</p>
</Checkbox> </Checkbox>
</div> </div>
<DialogFooter> <DialogFooter>
@@ -1,169 +0,0 @@
import { BLE } from "@components/PageComponents/Connect/BLE.tsx";
import { HTTP } from "@components/PageComponents/Connect/HTTP.tsx";
import { Serial } from "@components/PageComponents/Connect/Serial.tsx";
import {
Dialog,
DialogClose,
DialogContent,
DialogHeader,
DialogTitle,
} from "@components/UI/Dialog.tsx";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/UI/Tabs.tsx";
import {
type BrowserFeature,
useBrowserFeatureDetection,
} from "@core/hooks/useBrowserFeatureDetection.ts";
import { AlertCircle } from "lucide-react";
import { Trans, useTranslation } from "react-i18next";
import { Link } from "../UI/Typography/Link.tsx";
export interface TabElementProps {
closeDialog: () => void;
}
export interface TabManifest {
id: "HTTP" | "BLE" | "Serial";
label: string;
element: React.FC<TabElementProps>;
isDisabled: boolean;
}
export interface NewDeviceProps {
open: boolean;
onOpenChange: (open: boolean) => void;
}
interface FeatureErrorProps {
missingFeatures: BrowserFeature[];
tabId: "HTTP" | "BLE" | "Serial";
}
const errors: Record<BrowserFeature, { href: string; i18nKey: string }> = {
"Web Bluetooth": {
href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Bluetooth_API#browser_compatibility",
i18nKey: "newDeviceDialog.validation.requiresWebBluetooth",
},
"Web Serial": {
href: "https://developer.mozilla.org/en-US/docs/Web/API/Web_Serial_API#browser_compatibility",
i18nKey: "newDeviceDialog.validation.requiresWebSerial",
},
"Secure Context": {
href: "https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts",
i18nKey: "newDeviceDialog.validation.requiresSecureContext",
},
};
const ErrorMessage = ({ missingFeatures, tabId }: FeatureErrorProps) => {
if (missingFeatures.length === 0) {
return null;
}
const browserFeatures = missingFeatures.filter((feature) => feature !== "Secure Context");
const needsSecureContext = missingFeatures.includes("Secure Context");
const needsFeature =
tabId === "BLE" && browserFeatures.includes("Web Bluetooth")
? "Web Bluetooth"
: tabId === "Serial" && browserFeatures.includes("Web Serial")
? "Web Serial"
: undefined;
return (
<div className="flex flex-col items-start gap-2 bg-red-500 p-4 rounded-md text-sm text-slate-500 dark:text-slate-400">
<div className="flex items-center gap-2 w-full">
<AlertCircle size={40} className="mr-2 shrink-0 text-white" />
<div className="flex flex-col gap-3">
<div className="text-sm text-white">
{needsFeature && (
<Trans
i18nKey={errors[needsFeature].i18nKey}
components={[
<Link
key="0"
href={errors[needsFeature].href}
className="underline hover:text-slate-200 text-white dark:text-white dark:hover:text-slate-300"
/>,
]}
/>
)}
{needsFeature && needsSecureContext && " "}
{needsSecureContext && (
<Trans
i18nKey={
browserFeatures.length > 0
? "newDeviceDialog.validation.additionallyRequiresSecureContext"
: "newDeviceDialog.validation.requiresSecureContext"
}
components={{
"0": (
<Link
href={errors["Secure Context"].href}
className="underline hover:text-slate-200"
/>
),
}}
/>
)}
</div>
</div>
</div>
</div>
);
};
export const NewDeviceDialog = ({ open, onOpenChange }: NewDeviceProps) => {
const { t } = useTranslation("dialog");
const { unsupported } = useBrowserFeatureDetection();
const tabs: TabManifest[] = [
{
id: "HTTP",
label: t("newDeviceDialog.tabHttp"),
element: HTTP,
isDisabled: false,
},
{
id: "BLE",
label: t("newDeviceDialog.tabBluetooth"),
element: BLE,
isDisabled: unsupported.includes("Web Bluetooth") || unsupported.includes("Secure Context"),
},
{
id: "Serial",
label: t("newDeviceDialog.tabSerial"),
element: Serial,
isDisabled: unsupported.includes("Web Serial") || unsupported.includes("Secure Context"),
},
];
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent aria-describedby={undefined}>
<DialogClose />
<DialogHeader>
<DialogTitle>{t("newDeviceDialog.title")}</DialogTitle>
</DialogHeader>
<Tabs defaultValue="HTTP">
<TabsList>
{tabs.map((tab) => (
<TabsTrigger key={tab.id} value={tab.id}>
{tab.label}
</TabsTrigger>
))}
</TabsList>
{tabs.map((tab) => (
<TabsContent key={tab.id} value={tab.id}>
<fieldset disabled={tab.isDisabled}>
{tab.id !== "HTTP" && tab.isDisabled ? (
<ErrorMessage missingFeatures={unsupported} tabId={tab.id} />
) : (
<tab.element closeDialog={() => onOpenChange(false)} />
)}
</fieldset>
</TabsContent>
))}
</Tabs>
</DialogContent>
</Dialog>
);
};
@@ -27,10 +27,11 @@ 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, useDevice, useNodeDB } from "@core/stores"; import { useNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
import { useAppStore, useDevice } from "@core/stores";
import { cn } from "@core/utils/cn.ts"; import { cn } from "@core/utils/cn.ts";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/sdk";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { numberToHexUnpadded } from "@noble/curves/utils.js";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { fromByteArray } from "base64-js"; import { fromByteArray } from "base64-js";
import { import {
@@ -51,19 +52,25 @@ export interface NodeDetailsDialogProps {
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
} }
export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps) => { export const NodeDetailsDialog = ({
open,
onOpenChange,
}: NodeDetailsDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const { setDialogOpen, connection } = useDevice(); const { setDialogOpen, connection } = useDevice();
const { getNode } = useNodeDB();
const navigate = useNavigate(); const navigate = useNavigate();
const { setNodeNumToBeRemoved, nodeNumDetails } = useAppStore(); const { setNodeNumToBeRemoved, nodeNumDetails } = useAppStore();
const { updateFavorite } = useFavoriteNode(); const { updateFavorite } = useFavoriteNode();
const { updateIgnored } = useIgnoreNode(); const { updateIgnored } = useIgnoreNode();
const node = getNode(nodeNumDetails); const node = useNodeAsProto(nodeNumDetails);
const [isFavoriteState, setIsFavoriteState] = useState<boolean>(node?.isFavorite ?? false); const [isFavoriteState, setIsFavoriteState] = useState<boolean>(
const [isIgnoredState, setIsIgnoredState] = useState<boolean>(node?.isIgnored ?? false); node?.isFavorite ?? false,
);
const [isIgnoredState, setIsIgnoredState] = useState<boolean>(
node?.isIgnored ?? false,
);
useEffect(() => { useEffect(() => {
if (!node) { if (!node) {
@@ -178,7 +185,8 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
key: "batteryLevel", key: "batteryLevel",
label: t("nodeDetails.batteryLevel"), label: t("nodeDetails.batteryLevel"),
value: node.deviceMetrics?.batteryLevel, value: node.deviceMetrics?.batteryLevel,
format: (val: number) => (val === 101 ? t("batteryStatus.pluggedIn") : `${val.toFixed(2)}%`), format: (val: number) =>
val === 101 ? t("batteryStatus.pluggedIn") : `${val.toFixed(2)}%`,
}, },
{ {
key: "voltage", key: "voltage",
@@ -211,21 +219,35 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
<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 className="mr-1" name="message" onClick={handleDirectMessage}> <Button
className="mr-1"
name="message"
onClick={handleDirectMessage}
>
<MessageSquareIcon className="mr-2" /> <MessageSquareIcon className="mr-2" />
{t("nodeDetails.message")} {t("nodeDetails.message")}
</Button> </Button>
<Button className="mr-1" name="traceRoute" onClick={handleTraceroute}> <Button
className="mr-1"
name="traceRoute"
onClick={handleTraceroute}
>
<WaypointsIcon className="mr-2" /> <WaypointsIcon className="mr-2" />
{t("nodeDetails.traceRoute")} {t("nodeDetails.traceRoute")}
</Button> </Button>
<Button className="mr-1" onClick={handleToggleFavorite}> <Button className="mr-1" onClick={handleToggleFavorite}>
<StarIcon <StarIcon
className={cn(isFavoriteState ? " fill-yellow-400 stroke-yellow-400" : "")} className={cn(
isFavoriteState ? " fill-yellow-400 stroke-yellow-400" : "",
)}
/> />
</Button> </Button>
{node.user?.publicKey && node.user.publicKey.length > 0 && ( {node.user?.publicKey && node.user.publicKey.length > 0 && (
<Button className="mr-1" name="verifyKey" onClick={handleInitiateKeyVerification}> <Button
className="mr-1"
name="verifyKey"
onClick={handleInitiateKeyVerification}
>
<ShieldCheckIcon className="mr-2" /> <ShieldCheckIcon className="mr-2" />
{t("nodeDetails.verifyKey")} {t("nodeDetails.verifyKey")}
</Button> </Button>
@@ -248,7 +270,9 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
</Button> </Button>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent className="bg-slate-800 dark:bg-slate-600 text-white px-4 py-1 rounded text-xs"> <TooltipContent className="bg-slate-800 dark:bg-slate-600 text-white px-4 py-1 rounded text-xs">
{isIgnoredState ? t("nodeDetails.unignoreNode") : t("nodeDetails.ignoreNode")} {isIgnoredState
? t("nodeDetails.unignoreNode")
: t("nodeDetails.ignoreNode")}
<TooltipArrow className="fill-slate-800 dark:fill-slate-600" /> <TooltipArrow className="fill-slate-800 dark:fill-slate-600" />
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
@@ -278,7 +302,9 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
<div className="flex flex-col flex-wrap space-x-1 space-y-1"> <div className="flex flex-col flex-wrap space-x-1 space-y-1">
<div className="flex flex-row space-x-2"> <div className="flex flex-row space-x-2">
<div className="w-full bg-slate-100 text-slate-900 dark:text-slate-100 dark:bg-slate-800 p-3 rounded-lg"> <div className="w-full bg-slate-100 text-slate-900 dark:text-slate-100 dark:bg-slate-800 p-3 rounded-lg">
<p className="text-lg font-semibold">{t("nodeDetails.details")}</p> <p className="text-lg font-semibold">
{t("nodeDetails.details")}
</p>
<table className="table-fixed w-full"> <table className="table-fixed w-full">
<tbody> <tbody>
<tr> <tr>
@@ -292,10 +318,9 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
<tr> <tr>
<td>{t("nodeDetails.role")}</td> <td>{t("nodeDetails.role")}</td>
<td> <td>
{Protobuf.Config.Config_DeviceConfig_Role[node.user?.role ?? 0]?.replace( {Protobuf.Config.Config_DeviceConfig_Role[
/_/g, node.user?.role ?? 0
" ", ]?.replace(/_/g, " ")}
)}
</td> </td>
</tr> </tr>
<tr> <tr>
@@ -314,35 +339,44 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
<td>{t("nodeDetails.hardware")}</td> <td>{t("nodeDetails.hardware")}</td>
<td> <td>
{( {(
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ?? Protobuf.Mesh.HardwareModel[
t("unknown.shortName") node.user?.hwModel ?? 0
] ?? t("unknown.shortName")
).replace(/_/g, " ")} ).replace(/_/g, " ")}
</td> </td>
</tr> </tr>
<tr> <tr>
<td>{t("nodeDetails.messageable")}</td> <td>{t("nodeDetails.messageable")}</td>
<td>{node.user?.isUnmessagable ? t("no") : t("yes")}</td> <td>
{node.user?.isUnmessagable ? t("no") : t("yes")}
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<DeviceImage <DeviceImage
className="w-40 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={Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ?? "UNKNOWN"} deviceType={
Protobuf.Mesh.HardwareModel[node.user?.hwModel ?? 0] ??
"UNKNOWN"
}
/> />
</div> </div>
</div> </div>
<div> <div>
<div className={sectionClassName}> <div className={sectionClassName}>
<p className="text-lg font-semibold">{t("nodeDetails.security")}</p> <p className="text-lg font-semibold">
{t("nodeDetails.security")}
</p>
<table className="table-auto w-full"> <table className="table-auto w-full">
<tbody> <tbody>
<tr> <tr>
<td className="pr-2">{t("nodeDetails.publicKey")}</td> <td className="pr-2">{t("nodeDetails.publicKey")}</td>
<td> <td>
<pre className="text-xs pt-0.5"> <pre className="text-xs pt-0.5">
{node.user?.publicKey && node.user?.publicKey.length > 0 {node.user?.publicKey &&
node.user?.publicKey.length > 0
? fromByteArray(node.user.publicKey) ? fromByteArray(node.user.publicKey)
: t("unknown.longName")} : t("unknown.longName")}
</pre> </pre>
@@ -361,7 +395,9 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
</div> </div>
<div className={sectionClassName}> <div className={sectionClassName}>
<p className="text-lg font-semibold">{t("nodeDetails.position")}</p> <p className="text-lg font-semibold">
{t("nodeDetails.position")}
</p>
{node.position ? ( {node.position ? (
<table className="table-auto w-full"> <table className="table-auto w-full">
@@ -378,7 +414,8 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
{node.position.latitudeI / 1e7}, {node.position.longitudeI / 1e7} {node.position.latitudeI / 1e7},{" "}
{node.position.longitudeI / 1e7}
</a> </a>
</td> </td>
</tr> </tr>
@@ -397,7 +434,11 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
) : ( ) : (
<p>{t("unknown.longName")}</p> <p>{t("unknown.longName")}</p>
)} )}
<Button onClick={handleRequestPosition} name="requestPosition" className="mt-2"> <Button
onClick={handleRequestPosition}
name="requestPosition"
className="mt-2"
>
<MapPinnedIcon className="mr-2" /> <MapPinnedIcon className="mr-2" />
{t("nodeDetails.requestPosition")} {t("nodeDetails.requestPosition")}
</Button> </Button>
@@ -422,7 +463,9 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
<tr> <tr>
<td>{t("nodeDetails.uptime")}</td> <td>{t("nodeDetails.uptime")}</td>
<td> <td>
<Uptime seconds={node.deviceMetrics.uptimeSeconds} /> <Uptime
seconds={node.deviceMetrics.uptimeSeconds}
/>
</td> </td>
</tr> </tr>
)} )}
@@ -441,7 +484,9 @@ export const NodeDetailsDialog = ({ open, onOpenChange }: NodeDetailsDialogProps
</p> </p>
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="overflow-x-scroll"> <AccordionContent className="overflow-x-scroll">
<pre className="text-xs w-full">{JSON.stringify(node, null, 2)}</pre> <pre className="text-xs w-full">
{JSON.stringify(node, null, 2)}
</pre>
</AccordionContent> </AccordionContent>
</AccordionItem> </AccordionItem>
</Accordion> </Accordion>
@@ -8,7 +8,8 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@components/UI/Dialog.tsx"; } from "@components/UI/Dialog.tsx";
import { useDevice, useNodeDB } from "@core/stores"; import { useMyNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
import { useDevice } from "@core/stores";
import { fromByteArray } from "base64-js"; 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";
@@ -19,19 +20,25 @@ export interface PkiBackupDialogProps {
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
} }
export const PkiBackupDialog = ({ open, onOpenChange }: PkiBackupDialogProps) => { export const PkiBackupDialog = ({
open,
onOpenChange,
}: PkiBackupDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const { config, setDialogOpen } = useDevice(); const { config, setDialogOpen } = useDevice();
const { getMyNode } = useNodeDB(); const myNode = useMyNodeAsProto();
const privateKey = config.security?.privateKey; const privateKey = config.security?.privateKey;
const publicKey = config.security?.publicKey; const publicKey = config.security?.publicKey;
const decodeKeyData = React.useCallback((key: Uint8Array<ArrayBufferLike>) => { const decodeKeyData = React.useCallback(
if (!key) { (key: Uint8Array<ArrayBufferLike>) => {
return ""; if (!key) {
} return "";
return fromByteArray(key ?? new Uint8Array(0)); }
}, []); return fromByteArray(key ?? new Uint8Array(0));
},
[],
);
const closeDialog = React.useCallback(() => { const closeDialog = React.useCallback(() => {
setDialogOpen("pkiBackup", false); setDialogOpen("pkiBackup", false);
@@ -49,8 +56,8 @@ export const PkiBackupDialog = ({ open, onOpenChange }: PkiBackupDialogProps) =>
<head> <head>
<title>${t("pkiBackup.header", { <title>${t("pkiBackup.header", {
interpolation: { escapeValue: false }, interpolation: { escapeValue: false },
shortName: getMyNode()?.user?.shortName ?? t("unknown.shortName"), shortName: myNode?.user?.shortName ?? t("unknown.shortName"),
longName: getMyNode()?.user?.longName ?? t("unknown.longName"), longName: myNode?.user?.longName ?? t("unknown.longName"),
})}</title> })}</title>
<style> <style>
body { font-family: Arial, sans-serif; padding: 20px; } body { font-family: Arial, sans-serif; padding: 20px; }
@@ -61,8 +68,8 @@ export const PkiBackupDialog = ({ open, onOpenChange }: PkiBackupDialogProps) =>
<body> <body>
<h1>${t("pkiBackup.header", { <h1>${t("pkiBackup.header", {
interpolation: { escapeValue: false }, interpolation: { escapeValue: false },
shortName: getMyNode()?.user?.shortName ?? t("unknown.shortName"), shortName: myNode?.user?.shortName ?? t("unknown.shortName"),
longName: getMyNode()?.user?.longName ?? t("unknown.longName"), longName: myNode?.user?.longName ?? t("unknown.longName"),
})}</h1> })}</h1>
<h3>${t("pkiBackup.secureBackup")}</h3> <h3>${t("pkiBackup.secureBackup")}</h3>
<h3>${t("pkiBackup.publicKey")}</h3> <h3>${t("pkiBackup.publicKey")}</h3>
@@ -77,7 +84,7 @@ export const PkiBackupDialog = ({ open, onOpenChange }: PkiBackupDialogProps) =>
printWindow.print(); printWindow.print();
closeDialog(); closeDialog();
} }
}, [decodeKeyData, privateKey, publicKey, closeDialog, t, getMyNode]); }, [decodeKeyData, privateKey, publicKey, closeDialog, t, myNode]);
const createDownloadKeyFile = React.useCallback(() => { const createDownloadKeyFile = React.useCallback(() => {
if (!privateKey || !publicKey) { if (!privateKey || !publicKey) {
@@ -90,8 +97,8 @@ export const PkiBackupDialog = ({ open, onOpenChange }: PkiBackupDialogProps) =>
const formattedContent = [ const formattedContent = [
`${t("pkiBackup.header", { `${t("pkiBackup.header", {
interpolation: { escapeValue: false }, interpolation: { escapeValue: false },
shortName: getMyNode()?.user?.shortName ?? t("unknown.shortName"), shortName: myNode?.user?.shortName ?? t("unknown.shortName"),
longName: getMyNode()?.user?.longName ?? t("unknown.longName"), longName: myNode?.user?.longName ?? t("unknown.longName"),
})}\n\n`, })}\n\n`,
`${t("pkiBackup.privateKey")}\n`, `${t("pkiBackup.privateKey")}\n`,
decodedPrivateKey, decodedPrivateKey,
@@ -107,8 +114,8 @@ export const PkiBackupDialog = ({ open, onOpenChange }: PkiBackupDialogProps) =>
link.href = url; link.href = url;
link.download = t("pkiBackup.fileName", { link.download = t("pkiBackup.fileName", {
interpolation: { escapeValue: false }, interpolation: { escapeValue: false },
shortName: getMyNode()?.user?.shortName ?? t("unknown.shortName"), shortName: myNode?.user?.shortName ?? t("unknown.shortName"),
longName: getMyNode()?.user?.longName ?? t("unknown.longName"), longName: myNode?.user?.longName ?? t("unknown.longName"),
}); });
link.style.display = "none"; link.style.display = "none";
@@ -117,7 +124,7 @@ export const PkiBackupDialog = ({ open, onOpenChange }: PkiBackupDialogProps) =>
document.body.removeChild(link); document.body.removeChild(link);
closeDialog(); closeDialog();
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
}, [decodeKeyData, privateKey, publicKey, closeDialog, t, getMyNode]); }, [decodeKeyData, privateKey, publicKey, closeDialog, t, myNode]);
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
@@ -127,7 +134,9 @@ export const PkiBackupDialog = ({ open, onOpenChange }: PkiBackupDialogProps) =>
<DialogTitle>{t("pkiBackup.title")}</DialogTitle> <DialogTitle>{t("pkiBackup.title")}</DialogTitle>
<DialogDescription>{t("pkiBackup.secureBackup")}</DialogDescription> <DialogDescription>{t("pkiBackup.secureBackup")}</DialogDescription>
<DialogDescription> <DialogDescription>
<span className="font-bold break-before-auto">{t("pkiBackup.loseKeysWarning")}</span> <span className="font-bold break-before-auto">
{t("pkiBackup.loseKeysWarning")}
</span>
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter className="mt-6"> <DialogFooter className="mt-6">
@@ -46,7 +46,11 @@ export const PkiRegenerateDialog = ({
<DialogDescription>{dialogText.description}</DialogDescription> <DialogDescription>{dialogText.description}</DialogDescription>
</DialogHeader> </DialogHeader>
<DialogFooter> <DialogFooter>
<Button variant="destructive" name="regenerate" onClick={() => onSubmit()}> <Button
variant="destructive"
name="regenerate"
onClick={() => onSubmit()}
>
{dialogText.button} {dialogText.button}
</Button> </Button>
</DialogFooter> </DialogFooter>
+18 -9
View File
@@ -10,9 +10,10 @@ import {
} 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 { Label } from "@components/UI/Label.tsx"; import { Label } from "@components/UI/Label.tsx";
import { Protobuf, type Types } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/sdk";
import { useChannels } from "@meshtastic/sdk-react";
import { fromByteArray } from "base64-js"; import { fromByteArray } from "base64-js";
import { useEffect, useMemo, useState } from "react"; import { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { QRCode } from "react-qrcode-logo"; import { QRCode } from "react-qrcode-logo";
import { Checkbox } from "../UI/Checkbox/index.tsx"; import { Checkbox } from "../UI/Checkbox/index.tsx";
@@ -21,16 +22,15 @@ export interface QRDialogProps {
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
loraConfig?: Protobuf.Config.Config_LoRaConfig; loraConfig?: Protobuf.Config.Config_LoRaConfig;
channels: Map<Types.ChannelNumber, Protobuf.Channel.Channel>;
} }
export const QRDialog = ({ open, onOpenChange, loraConfig, channels }: QRDialogProps) => { export const QRDialog = ({ open, onOpenChange, loraConfig }: QRDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const [selectedChannels, setSelectedChannels] = useState<number[]>([0]); const [selectedChannels, setSelectedChannels] = useState<number[]>([0]);
const [qrCodeUrl, setQrCodeUrl] = useState<string>(""); const [qrCodeUrl, setQrCodeUrl] = useState<string>("");
const [qrCodeAdd, setQrCodeAdd] = useState<boolean>(); const [qrCodeAdd, setQrCodeAdd] = useState<boolean>();
const allChannels = useMemo(() => Array.from(channels.values()), [channels]); const allChannels = useChannels();
useEffect(() => { useEffect(() => {
const channelsToEncode = allChannels const channelsToEncode = allChannels
@@ -44,12 +44,16 @@ export const QRDialog = ({ open, onOpenChange, loraConfig, channels }: QRDialogP
settings: channelsToEncode, settings: channelsToEncode,
}), }),
); );
const base64 = fromByteArray(toBinary(Protobuf.AppOnly.ChannelSetSchema, encoded)) const base64 = fromByteArray(
toBinary(Protobuf.AppOnly.ChannelSetSchema, encoded),
)
.replace(/=/g, "") .replace(/=/g, "")
.replace(/\+/g, "-") .replace(/\+/g, "-")
.replace(/\//g, "_"); .replace(/\//g, "_");
setQrCodeUrl(`https://meshtastic.org/e/${qrCodeAdd ? "?add=true" : ""}#${base64}`); setQrCodeUrl(
`https://meshtastic.org/e/${qrCodeAdd ? "?add=true" : ""}#${base64}`,
);
}, [allChannels, selectedChannels, qrCodeAdd, loraConfig]); }, [allChannels, selectedChannels, qrCodeAdd, loraConfig]);
return ( return (
@@ -80,9 +84,14 @@ export const QRDialog = ({ open, onOpenChange, loraConfig, channels }: QRDialogP
checked={selectedChannels.includes(channel.index)} checked={selectedChannels.includes(channel.index)}
onChange={() => { onChange={() => {
if (selectedChannels.includes(channel.index)) { if (selectedChannels.includes(channel.index)) {
setSelectedChannels(selectedChannels.filter((c) => c !== channel.index)); setSelectedChannels(
selectedChannels.filter((c) => c !== channel.index),
);
} else { } else {
setSelectedChannels([...selectedChannels, channel.index]); setSelectedChannels([
...selectedChannels,
channel.index,
]);
} }
}} }}
/> />
@@ -1,5 +1,10 @@
import { act, fireEvent, render, screen } from "@testing-library/react"; import { act, fireEvent, render, screen } from "@testing-library/react";
import type { ButtonHTMLAttributes, ClassAttributes, InputHTMLAttributes, ReactNode } from "react"; import type {
ButtonHTMLAttributes,
ClassAttributes,
InputHTMLAttributes,
ReactNode,
} 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 { RebootDialog } from "./RebootDialog.tsx"; import { RebootDialog } from "./RebootDialog.tsx";
@@ -49,10 +54,16 @@ vi.mock("@components/UI/Input.tsx", async () => {
vi.mock("@components/UI/Dialog.tsx", () => { vi.mock("@components/UI/Dialog.tsx", () => {
return { return {
Dialog: ({ children }: { children: ReactNode }) => <div>{children}</div>, Dialog: ({ children }: { children: ReactNode }) => <div>{children}</div>,
DialogContent: ({ children }: { children: ReactNode }) => <div>{children}</div>, DialogContent: ({ children }: { children: ReactNode }) => (
DialogHeader: ({ children }: { children: ReactNode }) => <div>{children}</div>, <div>{children}</div>
),
DialogHeader: ({ children }: { children: ReactNode }) => (
<div>{children}</div>
),
DialogTitle: ({ children }: { children: ReactNode }) => <h1>{children}</h1>, DialogTitle: ({ children }: { children: ReactNode }) => <h1>{children}</h1>,
DialogDescription: ({ children }: { children: ReactNode }) => <p>{children}</p>, DialogDescription: ({ children }: { children: ReactNode }) => (
<p>{children}</p>
),
DialogClose: () => null, DialogClose: () => null,
}; };
}); });
@@ -70,8 +81,12 @@ describe("RebootDialog", () => {
it("renders dialog with default input value", () => { it("renders dialog with default input value", () => {
render(<RebootDialog open onOpenChange={() => {}} />); render(<RebootDialog open onOpenChange={() => {}} />);
expect(screen.getByPlaceholderText(/enter delay/i)).toHaveValue(5); expect(screen.getByPlaceholderText(/enter delay/i)).toHaveValue(5);
expect(screen.getByRole("heading", { name: /reboot device/i, level: 1 })).toBeInTheDocument(); expect(
expect(screen.getByRole("button", { name: /reboot now/i })).toBeInTheDocument(); screen.getByRole("heading", { name: /reboot device/i, level: 1 }),
).toBeInTheDocument();
expect(
screen.getByRole("button", { name: /reboot now/i }),
).toBeInTheDocument();
}); });
it("calls correct reboot function based on OTA checkbox state", () => { it("calls correct reboot function based on OTA checkbox state", () => {
@@ -184,6 +199,8 @@ describe("RebootDialog", () => {
fireEvent.click(screen.getByRole("button", { name: /cancel/i })); fireEvent.click(screen.getByRole("button", { name: /cancel/i }));
}); });
expect(rebootMock).toHaveBeenCalledWith(-1); expect(rebootMock).toHaveBeenCalledWith(-1);
expect(screen.queryByText(/reboot has been scheduled/i)).not.toBeInTheDocument(); expect(
screen.queryByText(/reboot has been scheduled/i),
).not.toBeInTheDocument();
}); });
}); });
@@ -96,7 +96,11 @@ export const RebootDialog = ({ open, onOpenChange }: RebootDialogProps) => {
<Separator /> <Separator />
{!isScheduled ? ( {!isScheduled ? (
<> <>
<Checkbox checked={isOTA} onChange={(checked) => setIsOTA(checked)} className="px-2"> <Checkbox
checked={isOTA}
onChange={(checked) => setIsOTA(checked)}
className="px-2"
>
{t("reboot.ota")} {t("reboot.ota")}
</Checkbox> </Checkbox>
<div className="flex gap-2 px-2 items-center relative"> <div className="flex gap-2 px-2 items-center relative">
@@ -133,7 +137,9 @@ export const RebootDialog = ({ open, onOpenChange }: RebootDialogProps) => {
) : ( ) : (
<div className="px-2"> <div className="px-2">
<div className="pb-6 pt-2 text-center"> <div className="pb-6 pt-2 text-center">
<Label className=" text-gray-700 dark:text-gray-300 ">{t("reboot.scheduled")}</Label> <Label className=" text-gray-700 dark:text-gray-300 ">
{t("reboot.scheduled")}
</Label>
</div> </div>
<Button <Button
variant="destructive" variant="destructive"
@@ -1,19 +1,13 @@
import { CurrentDeviceContext, useDeviceStore, useMessageStore } from "@core/stores"; import { CurrentDeviceContext, useDeviceStore } from "@core/stores";
import { MeshRegistry } from "@meshtastic/sdk";
import { MeshRegistryProvider } from "@meshtastic/sdk-react";
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 { RefreshKeysDialog } from "./RefreshKeysDialog.tsx"; import { RefreshKeysDialog } from "./RefreshKeysDialog.tsx";
import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts"; import { useRefreshKeysDialog } from "./useRefreshKeysDialog.ts";
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 mockUseRefreshKeysDialog = vi.mocked(useRefreshKeysDialog); const mockUseRefreshKeysDialog = vi.mocked(useRefreshKeysDialog);
const getInitialState = () => const getInitialState = () =>
@@ -31,22 +25,25 @@ afterEach(() => {
vi.restoreAllMocks(); vi.restoreAllMocks();
}); });
test("does not render dialog if no error exists for active chat", () => { test("does not render dialog when there are no node errors", () => {
const deviceId = 1; const deviceId = 1;
const activeChatNum = 54321;
useDeviceStore.getState().addDevice(deviceId); useDeviceStore.getState().addDevice(deviceId);
mockUseMessageStore.mockReturnValue({ activeChat: activeChatNum });
mockUseRefreshKeysDialog.mockReturnValue({ mockUseRefreshKeysDialog.mockReturnValue({
handleCloseDialog: vi.fn(), handleCloseDialog: vi.fn(),
handleNodeRemove: vi.fn(), handleNodeRemove: vi.fn(),
}); });
// Empty MeshRegistry so useNodeErrors returns the empty fallback.
const registry = new MeshRegistry();
const { container } = render( const { container } = render(
<CurrentDeviceContext.Provider value={{ deviceId }}> <MeshRegistryProvider registry={registry}>
<RefreshKeysDialog open onOpenChange={vi.fn()} /> <CurrentDeviceContext.Provider value={{ deviceId }}>
</CurrentDeviceContext.Provider>, <RefreshKeysDialog open onOpenChange={vi.fn()} />
</CurrentDeviceContext.Provider>
</MeshRegistryProvider>,
); );
expect(container.firstChild).toBeNull(); expect(container.firstChild).toBeNull();
@@ -6,7 +6,8 @@ import {
DialogHeader, DialogHeader,
DialogTitle, DialogTitle,
} from "@components/UI/Dialog.tsx"; } from "@components/UI/Dialog.tsx";
import { useMessages, useNodeDB } from "@core/stores"; import { useNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
import { useNodeErrors } from "@meshtastic/sdk-react";
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";
@@ -16,21 +17,20 @@ export interface RefreshKeysDialogProps {
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
} }
export const RefreshKeysDialog = ({ open, onOpenChange }: RefreshKeysDialogProps) => { export const RefreshKeysDialog = ({
open,
onOpenChange,
}: RefreshKeysDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const { activeChat } = useMessages(); const nodeError = useNodeErrors()[0];
const { nodeErrors, getNode } = useNodeDB(); const nodeWithError = useNodeAsProto(nodeError?.node ?? 0);
const { handleCloseDialog, handleNodeRemove } = useRefreshKeysDialog(); const { handleCloseDialog, handleNodeRemove } = useRefreshKeysDialog();
const nodeErrorNum = nodeErrors.get(activeChat); if (!nodeError) {
if (!nodeErrorNum) {
return null; return null;
} }
const nodeWithError = getNode(nodeErrorNum.node);
const text = { const text = {
title: t("refreshKeys.title", { title: t("refreshKeys.title", {
interpolation: { escapeValue: false }, interpolation: { escapeValue: false },
@@ -45,7 +45,10 @@ export const RefreshKeysDialog = ({ open, onOpenChange }: RefreshKeysDialogProps
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-8 flex flex-col gap-2" aria-describedby={undefined}> <DialogContent
className="max-w-8 flex flex-col gap-2"
aria-describedby={undefined}
>
<DialogClose onClick={handleCloseDialog} /> <DialogClose onClick={handleCloseDialog} />
<DialogHeader> <DialogHeader>
<DialogTitle>{text.title}</DialogTitle> <DialogTitle>{text.title}</DialogTitle>
@@ -54,17 +57,30 @@ export const RefreshKeysDialog = ({ open, onOpenChange }: RefreshKeysDialogProps
<ul className="mt-2"> <ul className="mt-2">
<li className="flex place-items-center gap-2 items-start"> <li className="flex place-items-center gap-2 items-start">
<div className="p-2 bg-slate-500 rounded-lg mt-1"> <div className="p-2 bg-slate-500 rounded-lg mt-1">
<LockKeyholeOpenIcon size={30} className="text-white justify-center" /> <LockKeyholeOpenIcon
size={30}
className="text-white justify-center"
/>
</div> </div>
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
<div> <div>
<p className="font-bold mb-0.5">{t("refreshKeys.label.acceptNewKeys")}</p> <p className="font-bold mb-0.5">
{t("refreshKeys.label.acceptNewKeys")}
</p>
<p>{t("refreshKeys.description.acceptNewKeys")}</p> <p>{t("refreshKeys.description.acceptNewKeys")}</p>
</div> </div>
<Button variant="default" name="requestNewKeys" onClick={handleNodeRemove}> <Button
variant="default"
name="requestNewKeys"
onClick={handleNodeRemove}
>
{t("button.requestNewKeys")} {t("button.requestNewKeys")}
</Button> </Button>
<Button variant="outline" name="dismiss" onClick={handleCloseDialog}> <Button
variant="outline"
name="dismiss"
onClick={handleCloseDialog}
>
{t("button.dismiss")} {t("button.dismiss")}
</Button> </Button>
</div> </div>
@@ -1,24 +1,22 @@
import { useDevice, useMessages, useNodeDB } from "@core/stores"; import { useDevice } from "@core/stores";
import { useActiveClient, useNodeErrors } from "@meshtastic/sdk-react";
import { useCallback } from "react"; import { useCallback } from "react";
export function useRefreshKeysDialog() { export function useRefreshKeysDialog() {
const { setDialogOpen } = useDevice(); const { setDialogOpen } = useDevice();
const { removeNode, clearNodeError, getNodeError } = useNodeDB(); const meshClient = useActiveClient();
const { activeChat } = useMessages(); const error = useNodeErrors()[0];
const handleCloseDialog = useCallback(() => { const handleCloseDialog = useCallback(() => {
setDialogOpen("refreshKeys", false); setDialogOpen("refreshKeys", false);
}, [setDialogOpen]); }, [setDialogOpen]);
const handleNodeRemove = useCallback(() => { const handleNodeRemove = useCallback(() => {
const nodeWithError = getNodeError(activeChat); if (!meshClient || !error) return;
if (!nodeWithError) { meshClient.nodes.clearError(error.node);
return;
}
clearNodeError(activeChat);
handleCloseDialog(); handleCloseDialog();
return removeNode(nodeWithError?.node); void meshClient.nodes.remove(error.node);
}, [activeChat, clearNodeError, getNodeError, removeNode, handleCloseDialog]); }, [meshClient, error, handleCloseDialog]);
return { return {
handleCloseDialog, handleCloseDialog,
@@ -1,5 +1,7 @@
import { Label } from "@components/UI/Label.tsx"; import { Label } from "@components/UI/Label.tsx";
import { useAppStore, useDevice, useNodeDB } from "@core/stores"; import { useNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
import { useAppStore } from "@core/stores";
import { useActiveClient } from "@meshtastic/sdk-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { DialogWrapper } from "./DialogWrapper.tsx"; import { DialogWrapper } from "./DialogWrapper.tsx";
@@ -8,15 +10,17 @@ export interface RemoveNodeDialogProps {
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
} }
export const RemoveNodeDialog = ({ open, onOpenChange }: RemoveNodeDialogProps) => { export const RemoveNodeDialog = ({
open,
onOpenChange,
}: RemoveNodeDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const { connection } = useDevice(); const meshClient = useActiveClient();
const { getNode, removeNode } = useNodeDB();
const { nodeNumToBeRemoved } = useAppStore(); const { nodeNumToBeRemoved } = useAppStore();
const node = useNodeAsProto(nodeNumToBeRemoved);
const handleConfirm = () => { const handleConfirm = () => {
connection?.removeNodeByNum(nodeNumToBeRemoved); void meshClient?.nodes.remove(nodeNumToBeRemoved);
removeNode(nodeNumToBeRemoved);
}; };
return ( return (
@@ -31,7 +35,7 @@ export const RemoveNodeDialog = ({ open, onOpenChange }: RemoveNodeDialogProps)
onConfirm={handleConfirm} onConfirm={handleConfirm}
> >
<div className="gap-4"> <div className="gap-4">
<Label>{getNode(nodeNumToBeRemoved)?.user?.longName}</Label> <Label>{node?.user?.longName}</Label>
</div> </div>
</DialogWrapper> </DialogWrapper>
); );
@@ -1,93 +1,70 @@
// ResetNodeDbDialog.test.tsx
import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest"; import { beforeEach, describe, expect, it, vi } from "vitest";
import { ResetNodeDbDialog } from "./ResetNodeDbDialog.tsx"; import { ResetNodeDbDialog } from "./ResetNodeDbDialog.tsx";
const mockResetNodes = vi.fn(); const mockResetNodes = vi.fn();
const mockDeleteAllMessages = vi.fn(); const mockClearAll = vi.fn();
const mockRemoveAllNodeErrors = vi.fn();
const mockRemoveAllNodes = vi.fn();
vi.mock("@core/stores", () => ({ const { mockUseActiveClient } = vi.hoisted(() => ({
CurrentDeviceContext: { mockUseActiveClient: vi.fn(),
_currentValue: { deviceId: 1234 }, }));
},
useDevice: () => ({ vi.mock("@meshtastic/sdk-react", () => ({
connection: { useActiveClient: mockUseActiveClient,
resetNodes: mockResetNodes,
},
}),
useMessages: () => ({
deleteAllMessages: mockDeleteAllMessages,
}),
useNodeDB: () => ({
removeAllNodeErrors: mockRemoveAllNodeErrors,
removeAllNodes: mockRemoveAllNodes,
}),
})); }));
describe("ResetNodeDbDialog", () => { describe("ResetNodeDbDialog", () => {
const mockOnOpenChange = vi.fn(); const mockOnOpenChange = vi.fn();
beforeEach(() => { beforeEach(() => {
mockOnOpenChange.mockClear(); vi.clearAllMocks();
mockResetNodes.mockClear(); mockUseActiveClient.mockReturnValue({
mockDeleteAllMessages.mockClear(); nodes: { reset: mockResetNodes },
mockRemoveAllNodeErrors.mockClear(); chat: { clearAll: mockClearAll },
mockRemoveAllNodes.mockClear(); });
}); });
it("calls resetNodes, closes dialog, and after resolve clears messages and node DB (with true flag)", async () => { it("calls reset({ keepMyNode: true }) then clears chat after resolve", async () => {
// Control the promise returned by resetNodes let resolveReset:
let resolveReset: (() => void) | undefined; | ((value: { status: "ok"; value: number }) => void)
| undefined;
mockResetNodes.mockImplementation( mockResetNodes.mockImplementation(
() => () =>
new Promise<void>((resolve) => { new Promise((resolve) => {
resolveReset = resolve; resolveReset = resolve;
}), }),
); );
mockClearAll.mockResolvedValue(undefined);
render(<ResetNodeDbDialog open onOpenChange={mockOnOpenChange} />); render(<ResetNodeDbDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(screen.getByRole("button", { name: "Reset Node Database" })); fireEvent.click(
screen.getByRole("button", { name: "Reset Node Database" }),
);
// Called immediately expect(mockResetNodes).toHaveBeenCalledWith({ keepMyNode: true });
expect(mockResetNodes).toHaveBeenCalledTimes(1);
// DialogWrapper awaits onConfirm (which returns undefined), so close happens on next microtask
await waitFor(() => { await waitFor(() => {
expect(mockOnOpenChange).toHaveBeenCalledTimes(1);
expect(mockOnOpenChange).toHaveBeenCalledWith(false); expect(mockOnOpenChange).toHaveBeenCalledWith(false);
}); });
// Nothing else should have happened yet (the promise hasn't resolved) expect(mockClearAll).not.toHaveBeenCalled();
expect(mockDeleteAllMessages).not.toHaveBeenCalled();
expect(mockRemoveAllNodeErrors).not.toHaveBeenCalled();
expect(mockRemoveAllNodes).not.toHaveBeenCalled();
// Resolve the reset resolveReset?.({ status: "ok", value: 1 });
resolveReset?.();
// Now the .then() chain should fire
await waitFor(() => { await waitFor(() => {
expect(mockDeleteAllMessages).toHaveBeenCalledTimes(1); expect(mockClearAll).toHaveBeenCalledTimes(1);
expect(mockRemoveAllNodeErrors).toHaveBeenCalledTimes(1);
expect(mockRemoveAllNodes).toHaveBeenCalledTimes(1);
expect(mockRemoveAllNodes).toHaveBeenCalledWith(true);
}); });
}); });
it("calls onOpenChange(false) and does not call resetNodes when cancel is clicked", async () => { it("does not call reset when cancel is clicked", async () => {
render(<ResetNodeDbDialog open onOpenChange={mockOnOpenChange} />); render(<ResetNodeDbDialog open onOpenChange={mockOnOpenChange} />);
fireEvent.click(screen.getByRole("button", { name: "Cancel" })); fireEvent.click(screen.getByRole("button", { name: "Cancel" }));
await waitFor(() => { await waitFor(() => {
expect(mockOnOpenChange).toHaveBeenCalledTimes(1);
expect(mockOnOpenChange).toHaveBeenCalledWith(false); expect(mockOnOpenChange).toHaveBeenCalledWith(false);
}); });
expect(mockResetNodes).not.toHaveBeenCalled(); expect(mockResetNodes).not.toHaveBeenCalled();
expect(mockDeleteAllMessages).not.toHaveBeenCalled(); expect(mockClearAll).not.toHaveBeenCalled();
expect(mockRemoveAllNodeErrors).not.toHaveBeenCalled();
expect(mockRemoveAllNodes).not.toHaveBeenCalled();
}); });
}); });
@@ -1,5 +1,5 @@
import { toast } from "@core/hooks/useToast.ts"; import { toast } from "@core/hooks/useToast.ts";
import { useDevice, useMessages, useNodeDB } from "@core/stores"; import { useActiveClient } from "@meshtastic/sdk-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { DialogWrapper } from "../DialogWrapper.tsx"; import { DialogWrapper } from "../DialogWrapper.tsx";
@@ -8,24 +8,23 @@ export interface ResetNodeDbDialogProps {
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
} }
export const ResetNodeDbDialog = ({ open, onOpenChange }: ResetNodeDbDialogProps) => { export const ResetNodeDbDialog = ({
open,
onOpenChange,
}: ResetNodeDbDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const { connection } = useDevice(); const meshClient = useActiveClient();
const { removeAllNodeErrors, removeAllNodes } = useNodeDB();
const { deleteAllMessages } = useMessages();
const handleResetNodeDb = () => { const handleResetNodeDb = () => {
connection if (!meshClient) return;
?.resetNodes() meshClient.nodes
.then(() => { .reset({ keepMyNode: true })
deleteAllMessages(); .then((result) => {
removeAllNodeErrors(); if (result.status === "error") throw result.error;
removeAllNodes(true); return meshClient.chat.clearAll();
}) })
.catch((error) => { .catch((error) => {
toast({ toast({ title: t("resetNodeDb.failedTitle") });
title: t("resetNodeDb.failedTitle"),
});
console.error("Failed to reset Node DB:", error); console.error("Failed to reset Node DB:", error);
}); });
}; };
@@ -1,6 +1,6 @@
import { useNodeDB } from "@core/stores"; import { useNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
import type { Protobuf, Types } from "@meshtastic/core"; import type { Protobuf, Types } from "@meshtastic/sdk";
import { numberToHexUnpadded } from "@noble/curves/abstract/utils"; import { numberToHexUnpadded } from "@noble/curves/utils.js";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TraceRoute } from "../PageComponents/Messages/TraceRoute.tsx"; import { TraceRoute } from "../PageComponents/Messages/TraceRoute.tsx";
@@ -25,19 +25,21 @@ export const TracerouteResponseDialog = ({
onOpenChange, onOpenChange,
}: TracerouteResponseDialogProps) => { }: TracerouteResponseDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const { getNode } = useNodeDB();
const route: number[] = traceroute?.data.route ?? []; const route: number[] = traceroute?.data.route ?? [];
const routeBack: number[] = traceroute?.data.routeBack ?? []; const routeBack: number[] = traceroute?.data.routeBack ?? [];
const snrTowards = (traceroute?.data.snrTowards ?? []).map((snr) => snr / 4); const snrTowards = (traceroute?.data.snrTowards ?? []).map((snr) => snr / 4);
const snrBack = (traceroute?.data.snrBack ?? []).map((snr) => snr / 4); const snrBack = (traceroute?.data.snrBack ?? []).map((snr) => snr / 4);
const from = getNode(traceroute?.to ?? 0); // The origin of the traceroute = the "to" node of the mesh packet const from = useNodeAsProto(traceroute?.to ?? 0); // The origin of the traceroute = the "to" node of the mesh packet
const fromLongName = const fromLongName =
from?.user?.longName ?? (from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName")); from?.user?.longName ??
(from ? `!${numberToHexUnpadded(from?.num)}` : t("unknown.shortName"));
const fromShortName = const fromShortName =
from?.user?.shortName ?? from?.user?.shortName ??
(from ? `${numberToHexUnpadded(from?.num).substring(0, 4)}` : t("unknown.shortName")); (from
? `${numberToHexUnpadded(from?.num).substring(0, 4)}`
: t("unknown.shortName"));
const toUser = getNode(traceroute?.from ?? 0); // The destination of the traceroute = the "from" node of the mesh packet const toUser = useNodeAsProto(traceroute?.from ?? 0); // The destination of the traceroute = the "from" node of the mesh packet
if (!toUser || !from) { if (!toUser || !from) {
return null; return null;
@@ -20,12 +20,16 @@ export interface RouterRoleDialogProps {
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
} }
export const UnsafeRolesDialog = ({ open, onOpenChange }: RouterRoleDialogProps) => { export const UnsafeRolesDialog = ({
open,
onOpenChange,
}: RouterRoleDialogProps) => {
const { t } = useTranslation("dialog"); const { t } = useTranslation("dialog");
const [confirmState, setConfirmState] = useState(false); const [confirmState, setConfirmState] = useState(false);
const { setDialogOpen } = useDevice(); const { setDialogOpen } = useDevice();
const deviceRoleLink = "https://meshtastic.org/docs/configuration/radio/device/"; const deviceRoleLink =
"https://meshtastic.org/docs/configuration/radio/device/";
const choosingTheRightDeviceRoleLink = const choosingTheRightDeviceRoleLink =
"https://meshtastic.org/blog/choosing-the-right-device-role/"; "https://meshtastic.org/blog/choosing-the-right-device-role/";
@@ -59,11 +63,17 @@ export const UnsafeRolesDialog = ({ open, onOpenChange }: RouterRoleDialogProps)
onChange={() => setConfirmState(!confirmState)} onChange={() => setConfirmState(!confirmState)}
name="confirmUnderstanding" name="confirmUnderstanding"
> >
<span className="dark:text-white">{t("unsafeRoles.confirmUnderstanding")}</span> <span className="dark:text-white">
{t("unsafeRoles.confirmUnderstanding")}
</span>
</Checkbox> </Checkbox>
</div> </div>
<DialogFooter className="mt-6"> <DialogFooter className="mt-6">
<Button variant="default" name="dismiss" onClick={() => handleCloseDialog("dismiss")}> <Button
variant="default"
name="dismiss"
onClick={() => handleCloseDialog("dismiss")}
>
{t("button.dismiss")} {t("button.dismiss")}
</Button> </Button>
<Button <Button
@@ -4,11 +4,20 @@ import {
} from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts"; } from "@components/Dialog/UnsafeRolesDialog/useUnsafeRolesDialog.ts";
import { eventBus } from "@core/utils/eventBus.ts"; import { eventBus } from "@core/utils/eventBus.ts";
import { renderHook } from "@testing-library/react"; import { renderHook } from "@testing-library/react";
import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from "vitest"; import {
afterEach,
beforeEach,
describe,
expect,
it,
type Mock,
vi,
} from "vitest";
const mockNavigate = vi.fn(); const mockNavigate = vi.fn();
vi.mock("@tanstack/react-router", async (importOriginal) => { vi.mock("@tanstack/react-router", async (importOriginal) => {
const actual = await importOriginal<typeof import("@tanstack/react-router")>(); const actual =
await importOriginal<typeof import("@tanstack/react-router")>();
return { return {
...actual, ...actual,
useNavigate: () => mockNavigate, useNavigate: () => mockNavigate,
@@ -56,7 +65,10 @@ describe("useUnsafeRolesDialog", () => {
result.current.handleCloseDialog(); result.current.handleCloseDialog();
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith("unsafeRoles", false); expect(mockDevice.setDialogOpen).toHaveBeenCalledWith(
"unsafeRoles",
false,
);
}); });
}); });
@@ -65,7 +77,8 @@ describe("useUnsafeRolesDialog", () => {
const { result } = renderUnsafeRolesHook(); const { result } = renderUnsafeRolesHook();
const safeRole = "SAFE_ROLE"; const safeRole = "SAFE_ROLE";
const validationResult = await result.current.validateRoleSelection(safeRole); const validationResult =
await result.current.validateRoleSelection(safeRole);
expect(validationResult).toBe(true); expect(validationResult).toBe(true);
expect(mockDevice.setDialogOpen).not.toHaveBeenCalled(); expect(mockDevice.setDialogOpen).not.toHaveBeenCalled();
@@ -74,40 +87,61 @@ describe("useUnsafeRolesDialog", () => {
it("should open dialog for unsafe roles and resolve with true when confirmed", async () => { it("should open dialog for unsafe roles and resolve with true when confirmed", async () => {
const { result } = renderUnsafeRolesHook(); const { result } = renderUnsafeRolesHook();
const validationPromise = result.current.validateRoleSelection(UNSAFE_ROLES[0]!); const validationPromise = result.current.validateRoleSelection(
UNSAFE_ROLES[0]!,
);
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith("unsafeRoles", true); expect(mockDevice.setDialogOpen).toHaveBeenCalledWith(
expect(eventBus.on).toHaveBeenCalledWith("dialog:unsafeRoles", expect.any(Function)); "unsafeRoles",
true,
);
expect(eventBus.on).toHaveBeenCalledWith(
"dialog:unsafeRoles",
expect.any(Function),
);
const onHandler = (eventBus.on as Mock).mock.calls[0]![1]; const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
onHandler({ action: "confirm" }); onHandler({ action: "confirm" });
const validationResult = await validationPromise; const validationResult = await validationPromise;
expect(validationResult).toBe(true); expect(validationResult).toBe(true);
expect(eventBus.off).toHaveBeenCalledWith("dialog:unsafeRoles", onHandler); expect(eventBus.off).toHaveBeenCalledWith(
"dialog:unsafeRoles",
onHandler,
);
}); });
it("should resolve with false when user dismisses the dialog", async () => { it("should resolve with false when user dismisses the dialog", async () => {
const { result } = renderUnsafeRolesHook(); const { result } = renderUnsafeRolesHook();
const validationPromise = result.current.validateRoleSelection(UNSAFE_ROLES[0]!); const validationPromise = result.current.validateRoleSelection(
UNSAFE_ROLES[0]!,
);
const onHandler = (eventBus.on as Mock).mock.calls[0]![1]; const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
onHandler({ action: "dismiss" }); onHandler({ action: "dismiss" });
const validationResult = await validationPromise; const validationResult = await validationPromise;
expect(validationResult).toBe(false); expect(validationResult).toBe(false);
expect(eventBus.off).toHaveBeenCalledWith("dialog:unsafeRoles", onHandler); expect(eventBus.off).toHaveBeenCalledWith(
"dialog:unsafeRoles",
onHandler,
);
}); });
it("should clean up event listener after response", async () => { it("should clean up event listener after response", async () => {
const { result } = renderUnsafeRolesHook(); const { result } = renderUnsafeRolesHook();
const validationPromise = result.current.validateRoleSelection(UNSAFE_ROLES[1]!); const validationPromise = result.current.validateRoleSelection(
UNSAFE_ROLES[1]!,
);
const onHandler = (eventBus.on as Mock).mock.calls[0]![1]; const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
onHandler({ action: "confirm" }); onHandler({ action: "confirm" });
await validationPromise; await validationPromise;
expect(eventBus.off).toHaveBeenCalledWith("dialog:unsafeRoles", onHandler); expect(eventBus.off).toHaveBeenCalledWith(
"dialog:unsafeRoles",
onHandler,
);
}); });
}); });
@@ -118,9 +152,13 @@ describe("useUnsafeRolesDialog", () => {
mockDevice.setDialogOpen.mockClear(); mockDevice.setDialogOpen.mockClear();
(eventBus.on as Mock).mockClear(); (eventBus.on as Mock).mockClear();
const validationPromise = result.current.validateRoleSelection(unsafeRole); const validationPromise =
result.current.validateRoleSelection(unsafeRole);
expect(mockDevice.setDialogOpen).toHaveBeenCalledWith("unsafeRoles", true); expect(mockDevice.setDialogOpen).toHaveBeenCalledWith(
"unsafeRoles",
true,
);
const onHandler = (eventBus.on as Mock).mock.calls[0]![1]; const onHandler = (eventBus.on as Mock).mock.calls[0]![1];
onHandler({ action: "confirm" }); onHandler({ action: "confirm" });
@@ -21,7 +21,11 @@ export const useUnsafeRolesDialog = () => {
setDialogOpen("unsafeRoles", true); setDialogOpen("unsafeRoles", true);
return new Promise((resolve) => { return new Promise((resolve) => {
const handleResponse = ({ action }: { action: "confirm" | "dismiss" }) => { const handleResponse = ({
action,
}: {
action: "confirm" | "dismiss";
}) => {
eventBus.off("dialog:unsafeRoles", handleResponse); eventBus.off("dialog:unsafeRoles", handleResponse);
resolve(action === "confirm"); resolve(action === "confirm");
}; };
+7 -2
View File
@@ -62,7 +62,9 @@ export interface UseDialogReturn {
} }
export const useDialog = (initialState?: DialogState): UseDialogReturn => { export const useDialog = (initialState?: DialogState): UseDialogReturn => {
const [state, setState] = useState<DialogState>(initialState ?? { isOpen: false }); const [state, setState] = useState<DialogState>(
initialState ?? { isOpen: false },
);
const openDialog = useCallback((config: DialogConfig) => { const openDialog = useCallback((config: DialogConfig) => {
setState({ isOpen: true, config }); setState({ isOpen: true, config });
@@ -95,7 +97,10 @@ export const useDialog = (initialState?: DialogState): UseDialogReturn => {
}; };
}; };
export const getDefaultTexts = (type: DialogType, t: ReturnType<typeof useTranslation>["t"]) => { export const getDefaultTexts = (
type: DialogType,
t: ReturnType<typeof useTranslation>["t"],
) => {
switch (type) { switch (type) {
case "confirm": case "confirm":
return { return {
+24 -7
View File
@@ -1,10 +1,13 @@
import { createZodResolver } from "@components/Form/createZodResolver.ts"; import { createZodResolver } from "@components/Form/createZodResolver.ts";
import { DynamicFormField, type FieldProps } from "@components/Form/DynamicFormField.tsx"; import {
DynamicFormField,
type FieldProps,
} from "@components/Form/DynamicFormField.tsx";
import { FieldWrapper } from "@components/Form/FormWrapper.tsx"; 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 { useEffect } from "react"; import { type ReactNode, useEffect } from "react";
import { import {
type Control, type Control,
type DefaultValues, type DefaultValues,
@@ -59,11 +62,16 @@ export interface DynamicFormProps<T extends FieldValues> {
valid?: boolean; valid?: boolean;
validationText?: string; validationText?: string;
fields: FieldProps<T>[]; fields: FieldProps<T>[];
/** Optional JSX rendered after the field list (e.g. action buttons that
* reach into the form via useFormContext). */
footer?: ReactNode;
}[]; }[];
validationSchema?: ZodType<T>; validationSchema?: ZodType<T>;
} }
export type DynamicFormFormInit<T extends FieldValues> = (methods: UseFormReturn<T, T, T>) => void; export type DynamicFormFormInit<T extends FieldValues> = (
methods: UseFormReturn<T, T, T>,
) => void;
export function DynamicForm<T extends FieldValues>({ export function DynamicForm<T extends FieldValues>({
propMethods, propMethods,
@@ -81,7 +89,9 @@ export function DynamicForm<T extends FieldValues>({
const internalMethods = useForm<T>({ const internalMethods = useForm<T>({
mode: "onChange", mode: "onChange",
defaultValues: defaultValues, defaultValues: defaultValues,
resolver: validationSchema ? createZodResolver(validationSchema) : undefined, resolver: validationSchema
? createZodResolver(validationSchema)
: undefined,
shouldFocusError: false, shouldFocusError: false,
resetOptions: { keepDefaultValues: true }, resetOptions: { keepDefaultValues: true },
values, values,
@@ -89,7 +99,8 @@ export function DynamicForm<T extends FieldValues>({
const methods = propMethods ?? internalMethods; const methods = propMethods ?? internalMethods;
const { handleSubmit, control, getValues, formState, getFieldState } = methods; const { handleSubmit, control, getValues, formState, getFieldState } =
methods;
useEffect(() => { useEffect(() => {
if (!propMethods) { if (!propMethods) {
@@ -97,7 +108,10 @@ export function DynamicForm<T extends FieldValues>({
} }
}, [onFormInit, propMethods, internalMethods]); }, [onFormInit, propMethods, internalMethods]);
const isDisabled = (disabledBy?: DisabledBy<T>[], disabled?: boolean): boolean => { const isDisabled = (
disabledBy?: DisabledBy<T>[],
disabled?: boolean,
): boolean => {
if (disabled) { if (disabled) {
return true; return true;
} }
@@ -114,7 +128,9 @@ export function DynamicForm<T extends FieldValues>({
return field.invert ? value : !value; return field.invert ? value : !value;
} }
if (typeof value === "number") { if (typeof value === "number") {
return field.invert ? field.selector !== value : field.selector === value; return field.invert
? field.selector !== value
: field.selector === value;
} }
return false; return false;
}); });
@@ -168,6 +184,7 @@ export function DynamicForm<T extends FieldValues>({
</FieldWrapper> </FieldWrapper>
); );
})} })}
{fieldGroup.footer}
</div> </div>
))} ))}
{hasSubmitButton && ( {hasSubmitButton && (
@@ -1,12 +1,24 @@
import { GenericInput, type InputFieldProps } from "@components/Form/FormInput.tsx"; import {
GenericInput,
type InputFieldProps,
} from "@components/Form/FormInput.tsx";
import { import {
PasswordGenerator, PasswordGenerator,
type PasswordGeneratorProps, type PasswordGeneratorProps,
} from "@components/Form/FormPasswordGenerator.tsx"; } from "@components/Form/FormPasswordGenerator.tsx";
import { type SelectFieldProps, SelectInput } from "@components/Form/FormSelect.tsx"; import {
import { type ToggleFieldProps, ToggleInput } from "@components/Form/FormToggle.tsx"; type SelectFieldProps,
SelectInput,
} from "@components/Form/FormSelect.tsx";
import {
type ToggleFieldProps,
ToggleInput,
} from "@components/Form/FormToggle.tsx";
import type { Control, FieldValues } from "react-hook-form"; import type { Control, FieldValues } from "react-hook-form";
import { type MultiSelectFieldProps, MultiSelectInput } from "./FormMultiSelect.tsx"; import {
type MultiSelectFieldProps,
MultiSelectInput,
} from "./FormMultiSelect.tsx";
export type FieldProps<T> = export type FieldProps<T> =
| InputFieldProps<T> | InputFieldProps<T>
@@ -34,7 +46,14 @@ export function DynamicFormField<T extends FieldValues>({
case "text": case "text":
case "password": case "password":
case "number": case "number":
return <GenericInput field={field} control={control} disabled={disabled} isDirty={isDirty} />; return (
<GenericInput
field={field}
control={control}
disabled={disabled}
isDirty={isDirty}
/>
);
case "toggle": case "toggle":
return ( return (
+18 -5
View File
@@ -48,7 +48,10 @@ export function GenericInput<T extends FieldValues>({
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newValue = e.target.value; const newValue = e.target.value;
if (field.properties?.fieldLength?.max && newValue.length > field.properties.fieldLength.max) { if (
field.properties?.fieldLength?.max &&
newValue.length > field.properties.fieldLength.max
) {
return; return;
} }
@@ -57,22 +60,32 @@ export function GenericInput<T extends FieldValues>({
} }
controllerField.onChange( controllerField.onChange(
field.type === "number" ? Number.parseFloat(newValue).toString() : newValue, field.type === "number"
? Number.parseFloat(newValue).toString()
: newValue,
); );
}; };
const currentLength = controllerField.value ? String(controllerField.value).length : 0; const currentLength = controllerField.value
? String(controllerField.value).length
: 0;
return ( return (
<div className="relative w-full"> <div className="relative w-full">
<Input <Input
type={field.type} type={field.type}
step={field.properties?.step} step={field.properties?.step}
value={field.type === "number" ? String(controllerField.value) : controllerField.value} value={
field.type === "number"
? String(controllerField.value)
: controllerField.value
}
id={field.name} id={field.name}
onChange={handleInputChange} onChange={handleInputChange}
showCopyButton={field.properties?.showCopyButton} showCopyButton={field.properties?.showCopyButton}
showPasswordToggle={field.properties?.showPasswordToggle || field.type === "password"} showPasswordToggle={
field.properties?.showPasswordToggle || field.type === "password"
}
className={field.properties?.className} className={field.properties?.className}
{...restProperties} {...restProperties}
disabled={disabled} disabled={disabled}
@@ -33,17 +33,20 @@ export function MultiSelectInput<T extends FieldValues>({
Object.values(enumValue)[0] !== null && Object.values(enumValue)[0] !== null &&
"i18nKey" in Object.values(enumValue)[0]; "i18nKey" in Object.values(enumValue)[0];
const optionsToRender = Object.entries(enumValue).map(([key, configOrValue]) => { const optionsToRender = Object.entries(enumValue).map(
if (isNewConfigStructure) { ([key, configOrValue]) => {
const config = configOrValue as (typeof FLAGS_CONFIG)[keyof typeof FLAGS_CONFIG]; if (isNewConfigStructure) {
return { const config =
key, configOrValue as (typeof FLAGS_CONFIG)[keyof typeof FLAGS_CONFIG];
display: t(config.i18nKey), return {
value: config.value, key,
}; display: t(config.i18nKey),
} value: config.value,
return { key, display: key, value: configOrValue as number }; };
}); }
return { key, display: key, value: configOrValue as number };
},
);
return ( return (
<MultiSelect <MultiSelect
+10 -3
View File
@@ -49,8 +49,13 @@ export function SelectInput<T extends FieldValues>({
control, control,
}); });
const { enumValue, formatEnumName, defaultValue, className, ...remainingProperties } = const {
field.properties; enumValue,
formatEnumName,
defaultValue,
className,
...remainingProperties
} = field.properties;
const valueToKeyMap: Record<string, string> = {}; const valueToKeyMap: Record<string, string> = {};
const optionsEnumValues: [string, number][] = []; const optionsEnumValues: [string, number][] = [];
@@ -95,7 +100,9 @@ export function SelectInput<T extends FieldValues>({
className={cn([ className={cn([
className, className,
isDirty ? "focus:ring-sky-500 ring-sky-500 ring-2 ring-offset-2" : "", isDirty ? "focus:ring-sky-500 ring-sky-500 ring-2 ring-offset-2" : "",
invalid ? "focus:ring-red-500 ring-red-500 ring-2 outline-offset-2" : "", invalid
? "focus:ring-red-500 ring-red-500 ring-2 outline-offset-2"
: "",
])} ])}
ref={ref} ref={ref}
onBlur={onBlur} onBlur={onBlur}
+6 -2
View File
@@ -34,8 +34,12 @@ export function ToggleInput<T extends FieldValues>({
{...field.properties} {...field.properties}
className={cn([ className={cn([
field.properties?.className, field.properties?.className,
isDirty ? "focus:ring-sky-500 ring-sky-500 ring-2 ring-offset-2" : "", isDirty
invalid ? "focus:ring-red-500 ring-red-500 ring-2 ring-offset-2" : "", ? "focus:ring-sky-500 ring-sky-500 ring-2 ring-offset-2"
: "",
invalid
? "focus:ring-red-500 ring-red-500 ring-2 ring-offset-2"
: "",
])} ])}
{...rest} {...rest}
/> />
+3 -1
View File
@@ -24,7 +24,9 @@ export const FieldWrapper = ({
<div className="grid grid-cols-1 lg:grid-cols-[0.6fr_2fr_.1fr] sm:items-baseline gap-4"> <div className="grid grid-cols-1 lg:grid-cols-[0.6fr_2fr_.1fr] sm:items-baseline gap-4">
<Label htmlFor={fieldName}>{label}</Label> <Label htmlFor={fieldName}>{label}</Label>
<div className="max-w-3xl"> <div className="max-w-3xl">
<p className="text-sm text-slate-500 dark:text-slate-400">{description}</p> <p className="text-sm text-slate-500 dark:text-slate-400">
{description}
</p>
<p hidden={valid ?? true} className="text-sm text-red-500"> <p hidden={valid ?? true} className="text-sm text-red-500">
{validationText} {validationText}
</p> </p>
@@ -1,5 +1,6 @@
import type { import type {
FieldError, FieldError,
FieldErrors,
FieldValues, FieldValues,
Resolver, Resolver,
ResolverOptions, ResolverOptions,
@@ -10,7 +11,11 @@ import type { ZodType } from "zod/v4";
export function createZodResolver<T extends FieldValues>( export function createZodResolver<T extends FieldValues>(
schema: ZodType<T, unknown>, schema: ZodType<T, unknown>,
): Resolver<T, unknown> { ): Resolver<T, unknown> {
return (values: T, _context: unknown, _options?: ResolverOptions<T>): ResolverResult<T> => { return (
values: T,
_context: unknown,
_options?: ResolverOptions<T>,
): ResolverResult<T> => {
const result = schema.safeParse(values); const result = schema.safeParse(values);
if (result.success) { if (result.success) {
return { return {
@@ -19,7 +24,10 @@ export function createZodResolver<T extends FieldValues>(
}; };
} }
const errors: Record<string, FieldError & { params?: Record<string, unknown> }> = {}; const errors: Record<
string,
FieldError & { params?: Record<string, unknown> }
> = {};
for (const issue of result.error.issues) { for (const issue of result.error.issues) {
const { path, code, message, ...params } = issue; const { path, code, message, ...params } = issue;
@@ -35,7 +43,8 @@ export function createZodResolver<T extends FieldValues>(
: ""; : "";
const newCode = const newCode =
code.replace(/_([a-z])/g, (_, char) => char.toUpperCase()) + (suffix ? `.${suffix}` : ""); code.replace(/_([a-z])/g, (_, char) => char.toUpperCase()) +
(suffix ? `.${suffix}` : "");
const fieldError: FieldError & { params?: Record<string, unknown> } = { const fieldError: FieldError & { params?: Record<string, unknown> } = {
type: newCode, type: newCode,
@@ -49,8 +58,8 @@ export function createZodResolver<T extends FieldValues>(
} }
return { return {
values: {} as T, values: {} as Record<string, never>,
errors, errors: errors as FieldErrors<T>,
}; };
}; };
} }
+12 -5
View File
@@ -18,7 +18,9 @@ interface LanguageSwitcherProps {
disableHover?: boolean; disableHover?: boolean;
} }
export default function LanguageSwitcher({ disableHover = false }: LanguageSwitcherProps) { export default function LanguageSwitcher({
disableHover = false,
}: LanguageSwitcherProps) {
const { i18n } = useTranslation("ui"); const { i18n } = useTranslation("ui");
const { set: setLanguage, current, getSupportedLangs } = useLang(); const { set: setLanguage, current, getSupportedLangs } = useLang();
@@ -44,13 +46,15 @@ export default function LanguageSwitcher({ disableHover = false }: LanguageSwitc
size={16} size={16}
className={cn( className={cn(
"text-gray-500 dark:text-gray-400 w-4 flex-shrink-0 transition-colors duration-150", "text-gray-500 dark:text-gray-400 w-4 flex-shrink-0 transition-colors duration-150",
!disableHover && "group-hover:text-gray-700 dark:group-hover:text-gray-200", !disableHover &&
"group-hover:text-gray-700 dark:group-hover:text-gray-200",
)} )}
/> />
<Subtle <Subtle
className={cn( className={cn(
"text-sm text-gray-600 dark:text-gray-100 transition-colors duration-150", "text-sm text-gray-600 dark:text-gray-100 transition-colors duration-150",
!disableHover && "group-hover:text-gray-800 dark:group-hover:text-gray-100", !disableHover &&
"group-hover:text-gray-800 dark:group-hover:text-gray-100",
)} )}
> >
{`${t("languagePicker.label")}:`} {`${t("languagePicker.label")}:`}
@@ -58,7 +62,8 @@ export default function LanguageSwitcher({ disableHover = false }: LanguageSwitc
<Subtle <Subtle
className={cn( className={cn(
"text-sm font-medium text-gray-700 dark:text-gray-200 transition-colors duration-150", "text-sm font-medium text-gray-700 dark:text-gray-200 transition-colors duration-150",
!disableHover && "group-hover:text-gray-900 dark:group-hover:text-white", !disableHover &&
"group-hover:text-gray-900 dark:group-hover:text-white",
)} )}
> >
{current?.name} {current?.name}
@@ -76,7 +81,9 @@ export default function LanguageSwitcher({ disableHover = false }: LanguageSwitc
<span>{language.flag}</span> <span>{language.flag}</span>
<span>{language.name}</span> <span>{language.name}</span>
</div> </div>
{i18n.language === language.code && <Check className="h-4 w-4 text-primary" />} {i18n.language === language.code && (
<Check className="h-4 w-4 text-primary" />
)}
</DropdownMenuItem> </DropdownMenuItem>
))} ))}
</DropdownMenuContent> </DropdownMenuContent>
+3 -1
View File
@@ -45,7 +45,9 @@ export const BaseMap = ({
const locale = useMemo(() => { const locale = useMemo(() => {
return { return {
"GeolocateControl.FindMyLocation": t("maplibre.GeolocateControl.FindMyLocation"), "GeolocateControl.FindMyLocation": t(
"maplibre.GeolocateControl.FindMyLocation",
),
"NavigationControl.ZoomIn": t("maplibre.NavigationControl.ZoomIn"), "NavigationControl.ZoomIn": t("maplibre.NavigationControl.ZoomIn"),
"NavigationControl.ZoomOut": t("maplibre.NavigationControl.ZoomOut"), "NavigationControl.ZoomOut": t("maplibre.NavigationControl.ZoomOut"),
"ScaleControl.Meters": t("unit.meter.suffix"), "ScaleControl.Meters": t("unit.meter.suffix"),
@@ -1,11 +1,17 @@
import { type ChannelValidation, makeChannelSchema } from "@app/validation/channel.ts"; import {
type ChannelValidation,
makeChannelSchema,
} from "@app/validation/channel.ts";
import { create } from "@bufbuild/protobuf"; import { create } from "@bufbuild/protobuf";
import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx"; import { PkiRegenerateDialog } from "@components/Dialog/PkiRegenerateDialog.tsx";
import { createZodResolver } from "@components/Form/createZodResolver.ts"; import { createZodResolver } from "@components/Form/createZodResolver.ts";
import { DynamicForm, type DynamicFormFormInit } from "@components/Form/DynamicForm.tsx"; import {
DynamicForm,
type DynamicFormFormInit,
} from "@components/Form/DynamicForm.tsx";
import { useDevice } from "@core/stores"; import { useDevice } from "@core/stores";
import { deepCompareConfig } from "@core/utils/deepCompareConfig.ts"; import { Protobuf } from "@meshtastic/sdk";
import { Protobuf } from "@meshtastic/core"; import { useConfigEditor, useSignal } from "@meshtastic/sdk-react";
import { fromByteArray, toByteArray } from "base64-js"; import { fromByteArray, toByteArray } from "base64-js";
import cryptoRandomString from "crypto-random-string"; import cryptoRandomString from "crypto-random-string";
import { useEffect, useMemo, useRef, useState } from "react"; import { useEffect, useMemo, useRef, useState } from "react";
@@ -17,8 +23,23 @@ export interface SettingsPanelProps {
channel: Protobuf.Channel.Channel; channel: Protobuf.Channel.Channel;
} }
const EMPTY_CHANNELS_SIGNAL = {
value: new Map<number, Protobuf.Channel.Channel>() as ReadonlyMap<
number,
Protobuf.Channel.Channel
>,
peek: () =>
new Map<number, Protobuf.Channel.Channel>() as ReadonlyMap<
number,
Protobuf.Channel.Channel
>,
subscribe: () => () => {},
} as const;
export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => { export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
const { config, setChange, getChange, removeChange } = useDevice(); const { config } = useDevice();
const editor = useConfigEditor();
const editorChannels = useSignal(editor?.channels ?? EMPTY_CHANNELS_SIGNAL);
const { t } = useTranslation(["channels", "ui", "dialog"]); const { t } = useTranslation(["channels", "ui", "dialog"]);
const defaultConfig = channel; const defaultConfig = channel;
@@ -31,7 +52,8 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
moduleSettings: { moduleSettings: {
...defaultConfig?.settings?.moduleSettings, ...defaultConfig?.settings?.moduleSettings,
positionPrecision: positionPrecision:
defaultConfig?.settings?.moduleSettings?.positionPrecision === undefined defaultConfig?.settings?.moduleSettings?.positionPrecision ===
undefined
? 10 ? 10
: defaultConfig?.settings?.moduleSettings?.positionPrecision, : defaultConfig?.settings?.moduleSettings?.positionPrecision,
}, },
@@ -39,10 +61,7 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
}, },
}; };
const workingChannel = getChange({ const workingChannel = editorChannels.get(channel.index);
type: "channel",
index: channel.index,
}) as Protobuf.Channel.Channel | undefined;
const effectiveConfig = workingChannel ?? channel; const effectiveConfig = workingChannel ?? channel;
const formValues = { const formValues = {
...effectiveConfig, ...effectiveConfig,
@@ -53,7 +72,8 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
moduleSettings: { moduleSettings: {
...effectiveConfig?.settings?.moduleSettings, ...effectiveConfig?.settings?.moduleSettings,
positionPrecision: positionPrecision:
effectiveConfig?.settings?.moduleSettings?.positionPrecision === undefined effectiveConfig?.settings?.moduleSettings?.positionPrecision ===
undefined
? 10 ? 10
: effectiveConfig?.settings?.moduleSettings?.positionPrecision, : effectiveConfig?.settings?.moduleSettings?.positionPrecision,
}, },
@@ -61,8 +81,11 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
}, },
}; };
const [preSharedDialogOpen, setPreSharedDialogOpen] = useState<boolean>(false); const [preSharedDialogOpen, setPreSharedDialogOpen] =
const [byteCount, setBytes] = useState<number>(effectiveConfig?.settings?.psk.length ?? 16); useState<boolean>(false);
const [byteCount, setBytes] = useState<number>(
effectiveConfig?.settings?.psk.length ?? 16,
);
const ChannelValidationSchema = useMemo(() => { const ChannelValidationSchema = useMemo(() => {
return makeChannelSchema(byteCount); return makeChannelSchema(byteCount);
}, [byteCount]); }, [byteCount]);
@@ -95,7 +118,7 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
}, [effectiveByteCount, trigger]); }, [effectiveByteCount, trigger]);
const onSubmit = (data: ChannelValidation) => { const onSubmit = (data: ChannelValidation) => {
if (!formState.isReady) { if (!formState.isReady || !editor) {
return; return;
} }
@@ -111,12 +134,7 @@ export const Channel = ({ onFormInit, channel }: SettingsPanelProps) => {
}, },
}); });
if (deepCompareConfig(channel, payload, true)) { editor.setChannel(payload);
removeChange({ type: "channel", index: channel.index });
return;
}
setChange({ type: "channel", index: channel.index }, payload, channel);
}; };
const preSharedKeyRegenerate = async () => { const preSharedKeyRegenerate = async () => {
@@ -1,9 +1,16 @@
import { Channel } from "@app/components/PageComponents/Channels/Channel"; import { Channel } from "@app/components/PageComponents/Channels/Channel";
import { create } from "@bufbuild/protobuf";
import { Button } from "@components/UI/Button.tsx"; import { Button } from "@components/UI/Button.tsx";
import { Spinner } from "@components/UI/Spinner.tsx"; import { Spinner } from "@components/UI/Spinner.tsx";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@components/UI/Tabs.tsx"; import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@components/UI/Tabs.tsx";
import { useDevice } from "@core/stores"; import { useDevice } from "@core/stores";
import type { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/sdk";
import { useChannels, useConfigEditor, useSignal } from "@meshtastic/sdk-react";
import i18next from "i18next"; import i18next from "i18next";
import { QrCodeIcon, UploadIcon } from "lucide-react"; import { QrCodeIcon, UploadIcon } from "lucide-react";
import { Suspense, useMemo } from "react"; import { Suspense, useMemo } from "react";
@@ -14,9 +21,12 @@ interface ConfigProps {
onFormInit: <T extends object>(methods: UseFormReturn<T>) => void; onFormInit: <T extends object>(methods: UseFormReturn<T>) => void;
} }
export const getChannelName = (channel: Protobuf.Channel.Channel) => { export const getChannelName = (channel: {
return channel.settings?.name.length index: number;
? channel.settings?.name settings?: { name?: string };
}) => {
return channel.settings?.name?.length
? channel.settings.name
: channel.index === 0 : channel.index === 0
? i18next.t("page.broadcastLabel") ? i18next.t("page.broadcastLabel")
: i18next.t("page.channelIndex", { : i18next.t("page.channelIndex", {
@@ -25,14 +35,41 @@ export const getChannelName = (channel: Protobuf.Channel.Channel) => {
}); });
}; };
const EMPTY_DIRTY_CHANNELS_SIGNAL = {
value: [] as readonly number[],
peek: () => [] as readonly number[],
subscribe: () => () => {},
} as const;
export const Channels = ({ onFormInit }: ConfigProps) => { export const Channels = ({ onFormInit }: ConfigProps) => {
const { channels, hasChannelChange, setDialogOpen } = useDevice(); const { setDialogOpen } = useDevice();
const editor = useConfigEditor();
const channels = useChannels();
const dirtyChannels = useSignal(
editor?.dirtyChannels ?? EMPTY_DIRTY_CHANNELS_SIGNAL,
);
const { t } = useTranslation("channels"); const { t } = useTranslation("channels");
const allChannels = Array.from(channels.values()); const allChannels = useMemo(
() =>
channels.map((c) =>
create(Protobuf.Channel.ChannelSchema, {
index: c.index,
role: c.role,
settings: c.settings,
}),
),
[channels],
);
const flags = useMemo( const flags = useMemo(
() => new Map(allChannels.map((channel) => [channel.index, hasChannelChange(channel.index)])), () =>
[allChannels, hasChannelChange], new Map(
allChannels.map((channel) => [
channel.index,
dirtyChannels.includes(channel.index),
]),
),
[allChannels, dirtyChannels],
); );
return ( return (
@@ -53,7 +90,10 @@ export const Channels = ({ onFormInit }: ConfigProps) => {
)} )}
</TabsTrigger> </TabsTrigger>
))} ))}
<Button className="ml-auto mr-1 h-8" onClick={() => setDialogOpen("import", true)}> <Button
className="ml-auto mr-1 h-8"
onClick={() => setDialogOpen("import", true)}
>
<UploadIcon className="mr-2" size={14} /> <UploadIcon className="mr-2" size={14} />
{t("page.import")} {t("page.import")}
</Button> </Button>
@@ -63,9 +103,16 @@ export const Channels = ({ onFormInit }: ConfigProps) => {
</Button> </Button>
</TabsList> </TabsList>
{allChannels.map((channel) => ( {allChannels.map((channel) => (
<TabsContent key={`channel_${channel.index}`} value={`channel_${channel.index}`}> <TabsContent
key={`channel_${channel.index}`}
value={`channel_${channel.index}`}
>
<Suspense fallback={<Spinner size="lg" className="my-5" />}> <Suspense fallback={<Spinner size="lg" className="my-5" />}>
<Channel key={channel.index} onFormInit={onFormInit} channel={channel} /> <Channel
key={channel.index}
onFormInit={onFormInit}
channel={channel}
/>
</Suspense> </Suspense>
</TabsContent> </TabsContent>
))} ))}
@@ -1,7 +1,11 @@
import { Button } from "@app/components/UI/Button"; import { Button } from "@app/components/UI/Button";
import type { Connection } from "@app/core/stores/deviceStore/types"; import type { Connection } from "@app/core/stores/deviceStore/types";
export function ConnectionStatusBadge({ status }: { status: Connection["status"] }) { export function ConnectionStatusBadge({
status,
}: {
status: Connection["status"];
}) {
let color = ""; let color = "";
let displayStatus = status; let displayStatus = status;
@@ -26,8 +30,13 @@ export function ConnectionStatusBadge({ status }: { status: Connection["status"]
} }
return ( return (
<Button variant="subtle" className="inline-flex items-center gap-2"> <Button variant="subtle" className="inline-flex items-center gap-2">
<span className={`h-2.5 w-2.5 rounded-full ${color}`} aria-hidden="true" /> <span
<span className="text-xs capitalize text-slate-500 dark:text-slate-400">{displayStatus}</span> className={`h-2.5 w-2.5 rounded-full ${color}`}
aria-hidden="true"
/>
<span className="text-xs capitalize text-slate-500 dark:text-slate-400">
{displayStatus}
</span>
</Button> </Button>
); );
} }
@@ -1,5 +1,5 @@
import { hasPos, toLngLat } from "@core/utils/geo"; import { hasPos, toLngLat } from "@core/utils/geo";
import type { Protobuf } from "@meshtastic/core"; import type { Protobuf } from "@meshtastic/sdk";
import type { Feature, FeatureCollection } from "geojson"; import type { Feature, FeatureCollection } from "geojson";
import type { HeatmapLayerSpecification } from "maplibre-gl"; import type { HeatmapLayerSpecification } from "maplibre-gl";
import { useMemo } from "react"; import { useMemo } from "react";
@@ -11,9 +11,16 @@ export interface HeatmapLayerProps {
id: string; id: string;
filteredNodes: Protobuf.Mesh.NodeInfo[]; filteredNodes: Protobuf.Mesh.NodeInfo[];
mode: HeatmapMode; mode: HeatmapMode;
isVisible: boolean;
} }
export const HeatmapLayer = ({ id, filteredNodes, mode }: HeatmapLayerProps) => { export const HeatmapLayer = ({
id,
filteredNodes,
mode,
isVisible,
}: HeatmapLayerProps) => {
if (!isVisible) return null;
const data: FeatureCollection = useMemo(() => { const data: FeatureCollection = useMemo(() => {
const features: Feature[] = filteredNodes const features: Feature[] = filteredNodes
.filter((node) => hasPos(node.position)) .filter((node) => hasPos(node.position))
@@ -41,7 +48,9 @@ export const HeatmapLayer = ({ id, filteredNodes, mode }: HeatmapLayerProps) =>
const paintProps: HeatmapLayerSpecification["paint"] = useMemo( const paintProps: HeatmapLayerSpecification["paint"] = useMemo(
() => ({ () => ({
"heatmap-weight": "heatmap-weight":
mode === "density" ? 1 : ["interpolate", ["linear"], ["get", "snr"], -20, 0, 10, 1], mode === "density"
? 1
: ["interpolate", ["linear"], ["get", "snr"], -20, 0, 10, 1],
"heatmap-intensity": ["interpolate", ["linear"], ["zoom"], 0, 1, 15, 3], "heatmap-intensity": ["interpolate", ["linear"], ["zoom"], 0, 1, 15, 3],
// Color ramp for heatmap. Domain is 0 (low) to 1 (high). // Color ramp for heatmap. Domain is 0 (low) to 1 (high).
// Begin color ramp at 0-stop with a 0-transparancy color // Begin color ramp at 0-stop with a 0-transparancy color
@@ -63,7 +72,17 @@ export const HeatmapLayer = ({ id, filteredNodes, mode }: HeatmapLayerProps) =>
1, 1,
"rgb(178,24,43)", "rgb(178,24,43)",
], ],
"heatmap-radius": ["interpolate", ["linear"], ["zoom"], 0, 2, 9, 20, 15, 30], "heatmap-radius": [
"interpolate",
["linear"],
["zoom"],
0,
2,
9,
20,
15,
30,
],
// Opacity 0.7 to be visible but not blocking // Opacity 0.7 to be visible but not blocking
"heatmap-opacity": 0.7, "heatmap-opacity": 0.7,
}), }),
@@ -13,9 +13,9 @@ import { NodeDetail } from "@components/PageComponents/Map/Popups/NodeDetail.tsx
import type { PopupState } from "@components/PageComponents/Map/Popups/PopupWrapper.tsx"; import type { PopupState } from "@components/PageComponents/Map/Popups/PopupWrapper.tsx";
import { PopupWrapper } from "@components/PageComponents/Map/Popups/PopupWrapper.tsx"; import { PopupWrapper } from "@components/PageComponents/Map/Popups/PopupWrapper.tsx";
import { useMapFitting } from "@core/hooks/useMapFitting"; import { useMapFitting } from "@core/hooks/useMapFitting";
import { useNodeDB } from "@core/stores";
import { hasPos, toLngLat } from "@core/utils/geo.ts"; import { hasPos, toLngLat } from "@core/utils/geo.ts";
import type { Protobuf } from "@meshtastic/core"; import type { Protobuf } from "@meshtastic/sdk";
import { useNodeErrors } from "@meshtastic/sdk-react";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { MapRef } from "react-map-gl/maplibre"; import type { MapRef } from "react-map-gl/maplibre";
@@ -43,14 +43,20 @@ export const NodesLayer = ({
}: NodeMarkerProps): React.ReactNode[] => { }: NodeMarkerProps): React.ReactNode[] => {
const { t } = useTranslation("map"); const { t } = useTranslation("map");
const { hasNodeError } = useNodeDB(); const errors = useNodeErrors();
const errorSet = useMemo(() => new Set(errors.map((e) => e.node)), [errors]);
const hasNodeError = useCallback(
(num: number) => errorSet.has(num),
[errorSet],
);
const { focusLngLat } = useMapFitting(mapRef); const { focusLngLat } = useMapFitting(mapRef);
const selectedNode = useMemo( const selectedNode = useMemo(
() => () =>
popupState?.type !== "node" popupState?.type !== "node"
? undefined ? undefined
: (filteredNodes.find((node) => node.num === popupState.num) ?? undefined), : (filteredNodes.find((node) => node.num === popupState.num) ??
undefined),
[popupState, filteredNodes], [popupState, filteredNodes],
); );
@@ -77,7 +83,9 @@ export const NodesLayer = ({
const isExpanded = expandedCluster === key; const isExpanded = expandedCluster === key;
// Precompute pixel offsets for expanded state // Precompute pixel offsets for expanded state
const expandedOffsets = isExpanded ? fanOutOffsetsPx(nodes.length, key) : undefined; const expandedOffsets = isExpanded
? fanOutOffsetsPx(nodes.length, key)
: undefined;
// Always render all node markers in the cluster // Always render all node markers in the cluster
for (const [i, node] of nodes.entries()) { for (const [i, node] of nodes.entries()) {
@@ -1,6 +1,6 @@
import { getColorFromNodeNum, isLightColor } from "@app/core/utils/color"; import { getColorFromNodeNum, isLightColor } from "@app/core/utils/color";
import { precisionBitsToMeters, toLngLat } from "@core/utils/geo.ts"; import { precisionBitsToMeters, toLngLat } from "@core/utils/geo.ts";
import type { Protobuf } from "@meshtastic/core"; import type { Protobuf } from "@meshtastic/sdk";
import { circle } from "@turf/turf"; import { circle } from "@turf/turf";
import type { Feature, FeatureCollection, Polygon } from "geojson"; import type { Feature, FeatureCollection, Polygon } from "geojson";
import { Layer, Source } from "react-map-gl/maplibre"; import { Layer, Source } from "react-map-gl/maplibre";
@@ -36,7 +36,10 @@ export function generatePrecisionCircles(
>(); >();
for (const node of filteredNodes) { for (const node of filteredNodes) {
if (node.position?.precisionBits === undefined || node.position.precisionBits === 0) { if (
node.position?.precisionBits === undefined ||
node.position.precisionBits === 0
) {
continue; continue;
} }
const [lng, lat] = toLngLat(node.position); const [lng, lat] = toLngLat(node.position);
@@ -60,7 +63,9 @@ export function generatePrecisionCircles(
} }
} }
const items = Array.from(unique.values()).sort((a, b) => a.radiusM - b.radiusM); const items = Array.from(unique.values()).sort(
(a, b) => a.radiusM - b.radiusM,
);
const features: Feature<Polygon, CircleProps>[] = items.map( const features: Feature<Polygon, CircleProps>[] = items.map(
({ lng, lat, radiusM, r, g, b, a }) => { ({ lng, lat, radiusM, r, g, b, a }) => {
@@ -91,7 +96,13 @@ export const SourcePrecisionCircles = ({
type="fill" type="fill"
layout={{ visibility: isVisible ? "visible" : "none" }} layout={{ visibility: isVisible ? "visible" : "none" }}
paint={{ paint={{
"fill-color": ["rgba", ["get", "r"], ["get", "g"], ["get", "b"], ["get", "a"]], "fill-color": [
"rgba",
["get", "r"],
["get", "g"],
["get", "b"],
["get", "a"],
],
}} }}
/> />
<Layer <Layer
@@ -11,7 +11,7 @@ import {
mercatorToLngLat, mercatorToLngLat,
toLngLat, toLngLat,
} from "@core/utils/geo"; } from "@core/utils/geo";
import type { Protobuf } from "@meshtastic/core"; import type { Protobuf } from "@meshtastic/sdk";
import type { Feature, FeatureCollection } from "geojson"; import type { Feature, FeatureCollection } from "geojson";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Layer, Source } from "react-map-gl/maplibre"; import { Layer, Source } from "react-map-gl/maplibre";
@@ -31,7 +31,7 @@ export interface SNRTooltipProps {
pos: { x: number; y: number }; pos: { x: number; y: number };
snr: number; snr: number;
from: string; from: string;
to: string; to: string | undefined;
} }
type NeighborPlus = Protobuf.Mesh.Neighbor & { type NeighborPlus = Protobuf.Mesh.Neighbor & {
@@ -64,7 +64,11 @@ type Pair = {
ba?: number; // SNR b->a ba?: number; // SNR b->a
}; };
function arcSegment(aLngLat: LngLat, bLngLat: LngLat, curved: boolean): LngLat[] | undefined { function arcSegment(
aLngLat: LngLat,
bLngLat: LngLat,
curved: boolean,
): LngLat[] | undefined {
if (!curved && distanceMeters(aLngLat, bLngLat) < MIN_LEN) { if (!curved && distanceMeters(aLngLat, bLngLat) < MIN_LEN) {
// Straight line // Straight line
return [aLngLat, bLngLat]; return [aLngLat, bLngLat];
@@ -176,7 +180,9 @@ function pushIfFeature(
} }
} }
function generateNeighborLines(neighborInfos: NeighborInfos[]): FeatureCollection { function generateNeighborLines(
neighborInfos: NeighborInfos[],
): FeatureCollection {
// Collect positions for all referenced nodes, discard pairs with missing positions // Collect positions for all referenced nodes, discard pairs with missing positions
const idToLngLat = new Map<number, LngLat>(); const idToLngLat = new Map<number, LngLat>();
const ensure = (node?: Protobuf.Mesh.NodeInfo | NeighborPlus) => { const ensure = (node?: Protobuf.Mesh.NodeInfo | NeighborPlus) => {
@@ -240,7 +246,12 @@ function generateNeighborLines(neighborInfos: NeighborInfos[]): FeatureCollectio
return { type: "FeatureCollection", features }; return { type: "FeatureCollection", features };
} }
export const SNRTooltip = ({ pos, snr, from, to }: Partial<SNRTooltipProps> = {}) => { export const SNRTooltip = ({
pos,
snr,
from,
to,
}: Partial<SNRTooltipProps> = {}) => {
const { t } = useTranslation(); const { t } = useTranslation();
if (!pos) { if (!pos) {
@@ -290,7 +301,9 @@ export const SNRLayer = ({
neighborInfo: { neighborInfo: {
...neighborInfo, ...neighborInfo,
neighbors: neighborInfo.neighbors.map((n) => { neighbors: neighborInfo.neighbors.map((n) => {
const node = filteredNodes.find((node) => node.num === n.nodeId); const node = filteredNodes.find(
(node) => node.num === n.nodeId,
);
return { ...n, num: node?.num, position: node?.position }; return { ...n, num: node?.num, position: node?.position };
}), }),
}, },
@@ -312,7 +325,10 @@ export const SNRLayer = ({
})) }))
: []; : [];
const featureCollection = generateNeighborLines([...remotePairs, ...directPairs]); const featureCollection = generateNeighborLines([
...remotePairs,
...directPairs,
]);
return ( return (
<Source type="geojson" data={featureCollection}> <Source type="geojson" data={featureCollection}>
@@ -4,7 +4,7 @@ import { PopupWrapper } from "@components/PageComponents/Map/Popups/PopupWrapper
import { WaypointDetail } from "@components/PageComponents/Map/Popups/WaypointDetail.tsx"; import { WaypointDetail } from "@components/PageComponents/Map/Popups/WaypointDetail.tsx";
import { useMapFitting } from "@core/hooks/useMapFitting"; import { useMapFitting } from "@core/hooks/useMapFitting";
import { useDevice, type WaypointWithMetadata } from "@core/stores"; import { useDevice, type WaypointWithMetadata } from "@core/stores";
import type { Protobuf } from "@meshtastic/core"; import type { Protobuf } from "@meshtastic/sdk";
import { useCallback } from "react"; import { useCallback } from "react";
import type { MapRef } from "react-map-gl/maplibre"; import type { MapRef } from "react-map-gl/maplibre";
@@ -82,7 +82,11 @@ export const WaypointLayer = ({
offset={[0, 25]} offset={[0, 25]}
onClose={() => setPopupState(undefined)} onClose={() => setPopupState(undefined)}
> >
<WaypointDetail waypoint={popupState.waypoint} myNode={myNode} onEdit={() => {}} /> <WaypointDetail
waypoint={popupState.waypoint}
myNode={myNode}
onEdit={() => {}}
/>
</PopupWrapper>, </PopupWrapper>,
); );
} }
@@ -8,7 +8,13 @@ type StackBadgeProps = {
onClick: (e: { originalEvent: MouseEvent }) => void; onClick: (e: { originalEvent: MouseEvent }) => void;
}; };
export const StackBadge = ({ lng, lat, count, isVisible = true, onClick }: StackBadgeProps) => { export const StackBadge = ({
lng,
lat,
count,
isVisible = true,
onClick,
}: StackBadgeProps) => {
return ( return (
<Marker <Marker
longitude={lng} longitude={lng}
@@ -6,8 +6,8 @@ import { Separator } from "@components/UI/Separator.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 { formatQuantity } from "@core/utils/string.ts"; import { formatQuantity } from "@core/utils/string.ts";
import type { Protobuf as ProtobufType } from "@meshtastic/core"; import type { Protobuf as ProtobufType } from "@meshtastic/sdk";
import { Protobuf } from "@meshtastic/core"; import { Protobuf } from "@meshtastic/sdk";
import { import {
Tooltip, Tooltip,
TooltipContent, TooltipContent,
@@ -16,7 +16,14 @@ import {
TooltipTrigger, TooltipTrigger,
} from "@radix-ui/react-tooltip"; } from "@radix-ui/react-tooltip";
import { useNavigate } from "@tanstack/react-router"; import { useNavigate } from "@tanstack/react-router";
import { Dot, LockIcon, LockOpenIcon, MessageSquareIcon, MountainSnow, Star } from "lucide-react"; import {
Dot,
LockIcon,
LockOpenIcon,
MessageSquareIcon,
MountainSnow,
Star,
} from "lucide-react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
export interface NodeDetailProps { export interface NodeDetailProps {
@@ -38,9 +45,16 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
: rawHardwareType.replaceAll("_", " ") : rawHardwareType.replaceAll("_", " ")
: `${hwModel}`; : `${hwModel}`;
// SNR is in dB; derive a 0–100% quality heuristic (-10 dB → 0%, +10 dB → 100%). // SNR is in dB; derive a 0–100% quality heuristic (-10 dB → 0%, +10 dB → 100%).
const snrQuality = Math.min(Math.max(Math.round((node.snr + 10) * 5), 0), 100); const snrQuality = Math.min(
Math.max(Math.round((node.snr + 10) * 5), 0),
100,
);
const snrTone = const snrTone =
snrQuality >= 67 ? "text-green-600" : snrQuality >= 34 ? "text-yellow-600" : "text-red-600"; snrQuality >= 67
? "text-green-600"
: snrQuality >= 34
? "text-yellow-600"
: "text-red-600";
function handleDirectMessage() { function handleDirectMessage() {
navigate({ to: `/messages/direct/${node.num}` }); navigate({ to: `/messages/direct/${node.num}` });
} }
@@ -101,7 +115,9 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
fill={node.isFavorite ? "black" : "none"} fill={node.isFavorite ? "black" : "none"}
size={15} size={15}
aria-label={ aria-label={
node.isFavorite ? t("nodeDetail.favorite.label") : t("nodeDetail.notFavorite.label") node.isFavorite
? t("nodeDetail.favorite.label")
: t("nodeDetail.notFavorite.label")
} }
/> />
</div> </div>
@@ -122,12 +138,15 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
<div <div
className="flex gap-1" className="flex gap-1"
title={new Date(node.lastHeard * 1000).toLocaleString(navigator.language)} title={new Date(node.lastHeard * 1000).toLocaleString(
navigator.language,
)}
> >
<div> <div>
{node.lastHeard > 0 && ( {node.lastHeard > 0 && (
<div> <div>
{t("nodeDetail.status.heard")} <TimeAgo timestamp={node.lastHeard * 1000} /> {t("nodeDetail.status.heard")}{" "}
<TimeAgo timestamp={node.lastHeard * 1000} />
</div> </div>
)} )}
</div> </div>
@@ -145,9 +164,13 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
<div className="flex mt-2 text-sm"> <div className="flex mt-2 text-sm">
<div className="flex items-center grow"> <div className="flex items-center grow">
<div className="border-2 border-slate-900 rounded-sm px-0.5 mr-1"> <div className="border-2 border-slate-900 rounded-sm px-0.5 mr-1">
{Number.isNaN(node.hopsAway) ? t("unit.hopsAway.unknown") : node.hopsAway} {Number.isNaN(node.hopsAway)
? t("unit.hopsAway.unknown")
: node.hopsAway}
</div>
<div>
{node.hopsAway === 1 ? t("unit.hop.one") : t("unit.hop.plural")}
</div> </div>
<div>{node.hopsAway === 1 ? t("unit.hop.one") : t("unit.hop.plural")}</div>
</div> </div>
{node.position?.altitude && ( {node.position?.altitude && (
<div className="flex items-center grow"> <div className="flex items-center grow">
@@ -170,13 +193,17 @@ export const NodeDetail = ({ node }: NodeDetailProps) => {
{!!node.deviceMetrics?.channelUtilization && ( {!!node.deviceMetrics?.channelUtilization && (
<div className="grow"> <div className="grow">
<div>{t("channelUtilization.short")}</div> <div>{t("channelUtilization.short")}</div>
<Mono>{node.deviceMetrics?.channelUtilization.toPrecision(3)}%</Mono> <Mono>
{node.deviceMetrics?.channelUtilization.toPrecision(3)}%
</Mono>
</div> </div>
)} )}
{!!node.deviceMetrics?.airUtilTx && ( {!!node.deviceMetrics?.airUtilTx && (
<div className="grow"> <div className="grow">
<div>{t("airtimeUtilization.short")}</div> <div>{t("airtimeUtilization.short")}</div>
<Mono className="text-gray-500">{node.deviceMetrics?.airUtilTx.toPrecision(3)}%</Mono> <Mono className="text-gray-500">
{node.deviceMetrics?.airUtilTx.toPrecision(3)}%
</Mono>
</div> </div>
)} )}
</div> </div>
@@ -1,9 +1,14 @@
import { TimeAgo } from "@components/generic/TimeAgo"; import { TimeAgo } from "@components/generic/TimeAgo";
import { Separator } from "@components/UI/Separator.tsx"; import { Separator } from "@components/UI/Separator.tsx";
import { useNodeAsProto } from "@core/hooks/useNodesAsProto.ts";
import type { WaypointWithMetadata } from "@core/stores"; import type { WaypointWithMetadata } from "@core/stores";
import { useNodeDB } from "@core/stores"; import {
import { bearingDegrees, distanceMeters, hasPos, toLngLat } from "@core/utils/geo"; bearingDegrees,
import type { Protobuf } from "@meshtastic/core"; distanceMeters,
hasPos,
toLngLat,
} from "@core/utils/geo";
import type { Protobuf } from "@meshtastic/sdk";
import { import {
ClockFadingIcon, ClockFadingIcon,
ClockPlusIcon, ClockPlusIcon,
@@ -24,7 +29,7 @@ interface WaypointDetailProps {
export const WaypointDetail = ({ waypoint, myNode }: WaypointDetailProps) => { export const WaypointDetail = ({ waypoint, myNode }: WaypointDetailProps) => {
const { t } = useTranslation("map"); const { t } = useTranslation("map");
const { getNode } = useNodeDB(); const lockedToNode = useNodeAsProto(waypoint.lockedTo ?? 0);
const waypointLngLat = toLngLat({ const waypointLngLat = toLngLat({
latitudeI: waypoint.latitudeI, latitudeI: waypoint.latitudeI,
@@ -83,10 +88,14 @@ export const WaypointDetail = ({ waypoint, myNode }: WaypointDetailProps) => {
<div className="flex flex-wrap items-start gap-x-3"> <div className="flex flex-wrap items-start gap-x-3">
<dt className="inline-flex items-center gap-2 text-slate-500 min-w-0"> <dt className="inline-flex items-center gap-2 text-slate-500 min-w-0">
<ClockPlusIcon size={14} aria-hidden /> <ClockPlusIcon size={14} aria-hidden />
<span className="truncate">{t("waypointDetail.createdDate")}</span> <span className="truncate">
{t("waypointDetail.createdDate")}
</span>
</dt> </dt>
<dd className="ms-auto text-right"> <dd className="ms-auto text-right">
<time dateTime={new Date(waypoint.metadata.created).toISOString()}> <time
dateTime={new Date(waypoint.metadata.created).toISOString()}
>
<TimeAgo timestamp={waypoint.metadata.created} /> <TimeAgo timestamp={waypoint.metadata.created} />
</time> </time>
</dd> </dd>
@@ -100,7 +109,9 @@ export const WaypointDetail = ({ waypoint, myNode }: WaypointDetailProps) => {
<span className="truncate">{t("waypointDetail.updated")}</span> <span className="truncate">{t("waypointDetail.updated")}</span>
</dt> </dt>
<dd className="ms-auto text-right"> <dd className="ms-auto text-right">
<time dateTime={new Date(waypoint.metadata.updated).toISOString()}> <time
dateTime={new Date(waypoint.metadata.updated).toISOString()}
>
<TimeAgo timestamp={waypoint.metadata.updated} /> <TimeAgo timestamp={waypoint.metadata.updated} />
</time> </time>
</dd> </dd>
@@ -132,7 +143,9 @@ export const WaypointDetail = ({ waypoint, myNode }: WaypointDetailProps) => {
<dd className="ms-auto text-right"> <dd className="ms-auto text-right">
<data value={Math.round(distance)}> <data value={Math.round(distance)}>
{Math.round(distance)}{" "} {Math.round(distance)}{" "}
{distance === 1 ? t("unit.meter.one") : t("unit.meter.plural")} {distance === 1
? t("unit.meter.one")
: t("unit.meter.plural")}
</data> </data>
</dd> </dd>
</div> </div>
@@ -166,7 +179,7 @@ export const WaypointDetail = ({ waypoint, myNode }: WaypointDetailProps) => {
<span className="truncate">{t("waypointDetail.lockedTo")}</span> <span className="truncate">{t("waypointDetail.lockedTo")}</span>
</dt> </dt>
<dd className="ms-auto text-right"> <dd className="ms-auto text-right">
{getNode(waypoint.lockedTo)?.user?.longName ?? t("unknown.longName")} {lockedToNode?.user?.longName ?? t("unknown.longName")}
</dd> </dd>
</div> </div>
)} )}
@@ -1,6 +1,10 @@
import type { HeatmapMode } from "@components/PageComponents/Map/Layers/HeatmapLayer.tsx"; import type { HeatmapMode } from "@components/PageComponents/Map/Layers/HeatmapLayer.tsx";
import { Checkbox } from "@components/UI/Checkbox/index.tsx"; import { Checkbox } from "@components/UI/Checkbox/index.tsx";
import { Popover, PopoverContent, PopoverTrigger } from "@components/UI/Popover.tsx"; import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@components/UI/Popover.tsx";
import { cn } from "@core/utils/cn.ts"; import { cn } from "@core/utils/cn.ts";
import { LayersIcon } from "lucide-react"; import { LayersIcon } from "lucide-react";
import { type ReactNode, useMemo } from "react"; import { type ReactNode, useMemo } from "react";
@@ -40,7 +44,12 @@ interface CheckboxProps {
className?: string; className?: string;
} }
const CheckboxItem = ({ label, checked, onChange, className }: CheckboxProps) => { const CheckboxItem = ({
label,
checked,
onChange,
className,
}: CheckboxProps) => {
return ( return (
<Checkbox <Checkbox
checked={checked} checked={checked}
@@ -140,7 +149,9 @@ export function MapLayerTool({
<CheckboxItem <CheckboxItem
label={label} label={label}
checked={visibilityState[key as keyof VisibilityState]} checked={visibilityState[key as keyof VisibilityState]}
onChange={() => handleCheckboxChange(key as keyof VisibilityState)} onChange={() =>
handleCheckboxChange(key as keyof VisibilityState)
}
/> />
{key === "heatmap" && visibilityState.heatmap && ( {key === "heatmap" && visibilityState.heatmap && (
<div className="pl-6 pt-2 flex flex-col gap-1"> <div className="pl-6 pt-2 flex flex-col gap-1">
@@ -151,7 +162,9 @@ export function MapLayerTool({
checked={heatmapMode === "density"} checked={heatmapMode === "density"}
onChange={() => setHeatmapMode("density")} onChange={() => setHeatmapMode("density")}
/> />
<span className="text-sm dark:text-slate-300">{t("layerTool.density")}</span> <span className="text-sm dark:text-slate-300">
{t("layerTool.density")}
</span>
</label> </label>
<label className="flex items-center gap-2 cursor-pointer"> <label className="flex items-center gap-2 cursor-pointer">
<input <input
@@ -1,4 +1,4 @@
import type { Protobuf } from "@meshtastic/core"; import type { Protobuf } from "@meshtastic/sdk";
export type ClusterKey = string; export type ClusterKey = string;
export type PxOffset = [number, number]; export type PxOffset = [number, number];

Some files were not shown because too many files have changed in this diff Show More