feat(protobufs): sync to firmware-current and consume workspace package (#1097)

* 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.
This commit is contained in:
Ben Meadors
2026-06-15 13:23:28 -05:00
committed by GitHub
parent 7a8529b841
commit bbe9a0d5cd
695 changed files with 27038 additions and 786 deletions
@@ -0,0 +1,171 @@
import {
fanOutOffsetsPx,
groupNodesByIdenticalCoords,
type PxOffset,
} from "@components/PageComponents/Map/cluster.ts";
import {
generatePrecisionCircles,
SourcePrecisionCircles,
} from "@components/PageComponents/Map/Layers/PrecisionLayer.tsx";
import { NodeMarker } from "@components/PageComponents/Map/Markers/NodeMarker.tsx";
import { StackBadge } from "@components/PageComponents/Map/Markers/StackBadge.tsx";
import { NodeDetail } from "@components/PageComponents/Map/Popups/NodeDetail.tsx";
import type { PopupState } from "@components/PageComponents/Map/Popups/PopupWrapper.tsx";
import { PopupWrapper } from "@components/PageComponents/Map/Popups/PopupWrapper.tsx";
import { useMapFitting } from "@core/hooks/useMapFitting";
import { useNodeDB } from "@core/stores";
import { hasPos, toLngLat } from "@core/utils/geo.ts";
import type { Protobuf } from "@meshtastic/core";
import { useCallback, useMemo } from "react";
import { useTranslation } from "react-i18next";
import type { MapRef } from "react-map-gl/maplibre";
export interface NodeMarkerProps {
mapRef: MapRef | undefined;
filteredNodes: Protobuf.Mesh.NodeInfo[];
myNode: Protobuf.Mesh.NodeInfo | undefined;
expandedCluster: string | undefined;
setExpandedCluster: (key: string | undefined) => void;
popupState: PopupState | undefined;
setPopupState: (state: PopupState | undefined) => void;
isVisible: boolean;
}
export const NodesLayer = ({
mapRef,
filteredNodes,
myNode,
expandedCluster,
setExpandedCluster,
popupState,
setPopupState,
isVisible,
}: NodeMarkerProps): React.ReactNode[] => {
const { t } = useTranslation("map");
const { hasNodeError } = useNodeDB();
const { focusLngLat } = useMapFitting(mapRef);
const selectedNode = useMemo(
() =>
popupState?.type !== "node"
? undefined
: (filteredNodes.find((node) => node.num === popupState.num) ?? undefined),
[popupState, filteredNodes],
);
const onMarkerClick = useCallback(
(num: number, offset: PxOffset, e: { originalEvent: MouseEvent }) => {
e.originalEvent?.stopPropagation();
setPopupState({ type: "node", num, offset });
const node = filteredNodes.find((node) => node.num === num) ?? undefined;
if (node) {
focusLngLat(toLngLat(node.position));
}
},
[filteredNodes, focusLngLat, setPopupState],
);
const clusters = groupNodesByIdenticalCoords(filteredNodes);
const rendered: React.ReactNode[] = [];
for (const [key, nodes] of clusters) {
if (!nodes.length || !nodes[0]?.position) {
continue;
}
const [lng, lat] = toLngLat(nodes[0].position);
const isExpanded = expandedCluster === key;
// Precompute pixel offsets for expanded state
const expandedOffsets = isExpanded ? fanOutOffsetsPx(nodes.length, key) : undefined;
// Always render all node markers in the cluster
for (const [i, node] of nodes.entries()) {
const isHead = i === 0;
rendered.push(
<NodeMarker
key={`node-${key}-${node.num}`}
id={node.num}
lng={lng}
lat={lat}
offset={expandedOffsets?.[i]}
label={node.user?.shortName ?? t("unknown.shortName")}
tooltipLabel={node.user?.longName ?? t("unknown.longName")}
hasError={hasNodeError(node.num)}
isFavorite={node.isFavorite ?? false}
isVisible={isVisible}
onClick={(num, e) => {
e.originalEvent?.stopPropagation();
if (!isExpanded && !isHead) {
// collapsed: tapping a buried marker expands the stack first
setExpandedCluster(key);
return;
}
onMarkerClick(num, expandedOffsets?.[i] ?? [0, 0], e);
}}
/>,
);
}
if (nodes.length > 1) {
rendered.push(
<StackBadge
key={`stack-badge-${key}`}
lng={lng}
lat={lat}
count={nodes.length - 1}
isVisible={isVisible && !isExpanded}
onClick={(e) => {
e.originalEvent?.stopPropagation();
setExpandedCluster(key);
}}
/>,
);
}
}
if (selectedNode) {
rendered.push(
<SourcePrecisionCircles
key={`precision-circles-selected-${selectedNode.num}`}
data={generatePrecisionCircles([selectedNode])}
id={`precisionCircles-selected-${selectedNode.num}`}
isVisible={true}
/>,
);
const [lng, lat] = toLngLat(selectedNode.position);
rendered.push(
<PopupWrapper
key={`popup-nodeinfo-${selectedNode.num}`}
lng={lng}
lat={lat}
offset={popupState?.type === "node" ? popupState.offset : [0, 0]}
onClose={() => setPopupState(undefined)}
>
<NodeDetail node={selectedNode} />
</PopupWrapper>,
);
}
if (myNode && hasPos(myNode.position)) {
const [lng, lat] = toLngLat(myNode.position);
rendered.push(
<NodeMarker
key={`node-${myNode.num}`}
id={myNode.num}
lng={lng}
lat={lat}
label={myNode.user?.shortName?.toString() ?? String(myNode.num)}
tooltipLabel={t("myNode.tooltip")}
hasError={false}
isFavorite={true}
onClick={(_, e) => onMarkerClick(myNode.num, [0, 0], e)}
/>,
);
}
return rendered;
};