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:
@@ -0,0 +1,172 @@
|
||||
import { DialogManager } from "@components/Dialog/DialogManager.tsx";
|
||||
import type { useAppStore, useMessageStore } from "@core/stores";
|
||||
import { Connections } from "@pages/Connections/index.tsx";
|
||||
import MapPage from "@pages/Map/index.tsx";
|
||||
import MessagesPage from "@pages/Messages.tsx";
|
||||
import NodesPage from "@pages/Nodes/index.tsx";
|
||||
import ConfigPage from "@pages/Settings/index.tsx";
|
||||
import {
|
||||
createRootRouteWithContext,
|
||||
createRoute,
|
||||
createRouter,
|
||||
redirect,
|
||||
} from "@tanstack/react-router";
|
||||
import type { useTranslation } from "react-i18next";
|
||||
import { z } from "zod/v4";
|
||||
import { App } from "./App.tsx";
|
||||
|
||||
interface AppContext {
|
||||
stores: {
|
||||
app: ReturnType<typeof useAppStore>;
|
||||
message: ReturnType<typeof useMessageStore>;
|
||||
};
|
||||
i18n: ReturnType<typeof useTranslation>;
|
||||
}
|
||||
|
||||
export const rootRoute = createRootRouteWithContext<AppContext>()({
|
||||
component: () => <App />,
|
||||
});
|
||||
|
||||
const indexRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/",
|
||||
component: Connections,
|
||||
loader: () => {
|
||||
// Redirect to the broadcast messages page on initial load
|
||||
return redirect({ to: "/messages/broadcast/0", replace: true });
|
||||
},
|
||||
});
|
||||
|
||||
const messagesRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/messages",
|
||||
component: MessagesPage,
|
||||
beforeLoad: ({ params }) => {
|
||||
const DEFAULT_CHANNEL = 0;
|
||||
|
||||
if (Object.values(params).length === 0) {
|
||||
throw redirect({
|
||||
to: `/messages/broadcast/${DEFAULT_CHANNEL}`,
|
||||
replace: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
export const messagesWithParamsRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/messages/$type/$chatId",
|
||||
component: MessagesPage,
|
||||
parseParams: (params) => ({
|
||||
type: z
|
||||
.enum(["direct", "broadcast"])
|
||||
.refine((val) => val === "direct" || val === "broadcast", {
|
||||
message: 'Type must be "direct" or "broadcast".',
|
||||
})
|
||||
.parse(params.type),
|
||||
chatId: z.coerce.number().int().min(0).max(4294967294).parse(params.chatId), // max is 0xffffffff - 1
|
||||
}),
|
||||
});
|
||||
|
||||
const mapRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/map",
|
||||
component: MapPage,
|
||||
});
|
||||
|
||||
const coordParamsSchema = z.object({
|
||||
// Accept "strings" from the URL, coerce to number, then validate
|
||||
long: z.coerce
|
||||
.number()
|
||||
.refine((n) => Number.isFinite(n) && n >= -180 && n <= 180, "Invalid longitude (-180..180)."),
|
||||
lat: z.coerce
|
||||
.number()
|
||||
.refine((n) => Number.isFinite(n) && n >= -90 && n <= 90, "Invalid latitude (-90..90)."),
|
||||
// Typical web map zoom levels ~0..22 (adjust if your map lib differs)
|
||||
zoom: z.coerce.number().int().min(0, "Zoom too small.").max(22, "Zoom too large."),
|
||||
});
|
||||
|
||||
export const mapWithParamsRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/map/$long/$lat/$zoom",
|
||||
component: MapPage,
|
||||
parseParams: (raw) => coordParamsSchema.parse(raw),
|
||||
// // This controls how params are serialized when you navigate/link
|
||||
// stringifyParams: (p) => ({
|
||||
// long: String(p.long),
|
||||
// lat: String(p.lat),
|
||||
// zoom: String(p.zoom),
|
||||
// }),
|
||||
});
|
||||
|
||||
export const settingsRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/settings",
|
||||
component: ConfigPage,
|
||||
// beforeLoad: () => {
|
||||
// throw redirect({
|
||||
// to: "/settings/radio",
|
||||
// replace: true,
|
||||
// });
|
||||
// },
|
||||
});
|
||||
|
||||
export const radioRoute = createRoute({
|
||||
getParentRoute: () => settingsRoute,
|
||||
path: "radio",
|
||||
component: ConfigPage,
|
||||
});
|
||||
|
||||
export const deviceRoute = createRoute({
|
||||
getParentRoute: () => settingsRoute,
|
||||
path: "device",
|
||||
component: ConfigPage,
|
||||
});
|
||||
|
||||
export const moduleRoute = createRoute({
|
||||
getParentRoute: () => settingsRoute,
|
||||
path: "module",
|
||||
component: ConfigPage,
|
||||
});
|
||||
|
||||
const nodesRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/nodes",
|
||||
component: NodesPage,
|
||||
});
|
||||
|
||||
const dialogWithParamsRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/dialog/$dialogId",
|
||||
component: DialogManager,
|
||||
});
|
||||
|
||||
const connectionsRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/connections",
|
||||
component: Connections,
|
||||
});
|
||||
|
||||
const routeTree = rootRoute.addChildren([
|
||||
indexRoute,
|
||||
messagesRoute,
|
||||
messagesWithParamsRoute,
|
||||
mapRoute,
|
||||
mapWithParamsRoute,
|
||||
settingsRoute.addChildren([radioRoute, deviceRoute, moduleRoute]),
|
||||
nodesRoute,
|
||||
dialogWithParamsRoute,
|
||||
connectionsRoute,
|
||||
]);
|
||||
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: {
|
||||
stores: {
|
||||
app: {} as ReturnType<typeof useAppStore>,
|
||||
message: {} as ReturnType<typeof useMessageStore>,
|
||||
},
|
||||
i18n: {} as ReturnType<typeof import("react-i18next").useTranslation>,
|
||||
},
|
||||
});
|
||||
export { router };
|
||||
Reference in New Issue
Block a user