* 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>
#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).
The Release Web workflow builds the Docker image with the repo root as
context (file: ./apps/web/infra/Containerfile), but the Containerfile's
ADD/COPY still referenced ./packages/web/dist and
./packages/web/infra/default.conf. After #1097 moved the web app to
apps/web, those paths no longer exist, so buildx failed with
'"/packages/web/infra/default.conf": not found'.
Point both at ./apps/web/. Verified with a local multi-stage docker
build against the apps/web build output.
* feat(protobufs): sync to firmware-current and consume workspace package
Sync the vendored .proto sources to firmware-current (v2.7.25+48), regenerate the v2 TS bindings, and consume the workspace @meshtastic/protobufs (workspace:*) in place of the stale JSR 2.7.20 — finishing the monorepo migration (core was already workspace:*).
Includes the one required breaking-change fix: admin nodedb_reset changed int32 to bool, so resetNodes() now sends value: true.
* build(protobufs): vendor generated bindings for workspace consumers
The package is consumed via workspace:* — its exports point at the TS source, which imports ./dist/meshtastic/*_pb.ts — so the generated output must exist at build time. CI builds web/core with no codegen step and the runners have no buf CLI, so the bindings are vendored here (kept gitignored; lint/format skip them). Regenerate with: pnpm --filter @meshtastic/protobufs gen
* fix(protobufs): clean script removes the actual generated output dir
buf writes bindings to packages/ts/dist, but clean was removing a non-existent root dist — so it never cleaned stale output. Addresses Copilot review feedback.
* refactor: move web app packages/web -> apps/web
Aligns the web app with the apps/web layout (matching the Vercel web-test Root Directory and the SDK-migration direction). Pure directory move plus root config: pnpm-workspace (adds apps/*), vitest projects, root tsconfig reference, and the pr/release-web/nightly workflows. vercel.json moved with the app. Build + 36 validation tests green.
* feat: config fields, module pages, key verification, telemetry capture
Incorporates the firmware-current feature work onto the protobuf foundation: new config fields (Display message bubbles; LoRa fem_lna_mode + serial_hal_only; Telemetry air_quality_screen_enabled); 4 new ModuleConfig pages (TrafficManagement, StatusMessage, TAK, RemoteHardware); the manual Key Verification flow (sendKeyVerification + ClientNotificationDialog stages + Verify Key button); live telemetry capture (nodeDB addDeviceMetrics) and admin hardening (toggleMutedNode, graceful PortNum default); plus the sdk-preview ConfigEditor demo and store/config tests. Build + lint + format + 131 tests green.
* chore: drop #1062 (unsaved-change-detection) to match upstream revert
#1062 was merged to main by accident (per @danditomaso) and is being reverted. Reverse-applied its diff here via 3-way so #1097 stays consistent with where main is headed, while keeping the feature changes layered on the same files (deviceStore/changeRegistry). Build + 131 tests + lint + format green.
* fix(nodes): clean up SNR display in node table and map popup
SNR is a ratio measured in dB, not dBm (which is absolute power); the
node table and map popup both mislabeled it and crammed three values
together: '0dBm/50%/50raw'. The trailing '%/raw' pair was the same
heuristic ((snr+10)*5) shown twice — once clamped, once not.
Render SNR in dB rounded to one decimal, color-coded by a 0-100%
signal-quality heuristic (green/yellow/red), with the quality percentage
as a muted secondary. Drop the redundant raw value. Adds unit.db; this
matches the existing SNRTooltip, which already renders dB.