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,215 @@
|
||||
import { useFirstSavedConnection } from "@app/core/stores/deviceStore/selectors.ts";
|
||||
import { SidebarButton } from "@components/UI/Sidebar/SidebarButton.tsx";
|
||||
import { SidebarSection } from "@components/UI/Sidebar/SidebarSection.tsx";
|
||||
import { Spinner } from "@components/UI/Spinner.tsx";
|
||||
import { Subtle } from "@components/UI/Typography/Subtle.tsx";
|
||||
import {
|
||||
type Page,
|
||||
useActiveConnection,
|
||||
useAppStore,
|
||||
useDefaultConnection,
|
||||
useDevice,
|
||||
useNodeDB,
|
||||
useSidebar,
|
||||
} from "@core/stores";
|
||||
import { cn } from "@core/utils/cn.ts";
|
||||
import { useLocation, useNavigate } from "@tanstack/react-router";
|
||||
import {
|
||||
CircleChevronLeft,
|
||||
type LucideIcon,
|
||||
MapIcon,
|
||||
MessageSquareIcon,
|
||||
SettingsIcon,
|
||||
UsersIcon,
|
||||
} from "lucide-react";
|
||||
import type React from "react";
|
||||
import { useEffect, useState, useTransition } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { DeviceInfoPanel } from "./DeviceInfoPanel.tsx";
|
||||
|
||||
export interface SidebarProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
interface NavLink {
|
||||
name: string;
|
||||
icon: LucideIcon;
|
||||
page: Page;
|
||||
count?: number;
|
||||
}
|
||||
|
||||
const CollapseToggleButton = () => {
|
||||
const { isCollapsed, toggleSidebar } = useSidebar();
|
||||
const { t } = useTranslation("ui");
|
||||
const buttonLabel = isCollapsed
|
||||
? t("sidebar.collapseToggle.button.open")
|
||||
: t("sidebar.collapseToggle.button.close");
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
aria-label={buttonLabel}
|
||||
onClick={toggleSidebar}
|
||||
className={cn(
|
||||
"absolute top-20 right-0 z-10 p-0.5 rounded-full transform translate-x-1/2",
|
||||
"transition-colors duration-300 ease-in-out",
|
||||
"border border-slate-300 dark:border-slate-200",
|
||||
"text-slate-500 dark:text-slate-200 hover:text-slate-400 dark:hover:text-slate-400",
|
||||
"focus:outline-none focus:ring-2 focus:ring-accent transition-transform bg-background-primary",
|
||||
)}
|
||||
>
|
||||
<CircleChevronLeft
|
||||
size={24}
|
||||
className={cn("transition-transform duration-300 ease-in-out", isCollapsed && "rotate-180")}
|
||||
/>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export const Sidebar = ({ children }: SidebarProps) => {
|
||||
const { hardware, metadata, unreadCounts, setDialogOpen } = useDevice();
|
||||
const { getNode, getNodesLength } = useNodeDB();
|
||||
const { setCommandPaletteOpen } = useAppStore();
|
||||
const myNode = getNode(hardware.myNodeNum);
|
||||
const { isCollapsed } = useSidebar();
|
||||
const { t } = useTranslation("ui");
|
||||
const navigate = useNavigate({ from: "/" });
|
||||
|
||||
// Get the active connection from selector (connected > default > first)
|
||||
const activeConnection =
|
||||
useActiveConnection() ||
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: not a react hook
|
||||
useDefaultConnection() ||
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: not a hook
|
||||
useFirstSavedConnection();
|
||||
|
||||
const pathname = useLocation({
|
||||
select: (location) => location.pathname.replace(/^\//, ""),
|
||||
});
|
||||
|
||||
const myMetadata = metadata.get(0);
|
||||
|
||||
const numUnread = [...unreadCounts.values()].reduce((sum, v) => sum + v, 0);
|
||||
|
||||
const [displayedNodeCount, setDisplayedNodeCount] = useState(() =>
|
||||
Math.max(getNodesLength() - 1, 0),
|
||||
);
|
||||
|
||||
const [_, startNodeCountTransition] = useTransition();
|
||||
|
||||
const currentNodeCountValue = Math.max(getNodesLength() - 1, 0);
|
||||
|
||||
useEffect(() => {
|
||||
if (currentNodeCountValue !== displayedNodeCount) {
|
||||
startNodeCountTransition(() => {
|
||||
setDisplayedNodeCount(currentNodeCountValue);
|
||||
});
|
||||
}
|
||||
}, [currentNodeCountValue, displayedNodeCount]);
|
||||
|
||||
const pages: NavLink[] = [
|
||||
{
|
||||
name: t("navigation.messages"),
|
||||
icon: MessageSquareIcon,
|
||||
page: "messages",
|
||||
count: numUnread ? numUnread : undefined,
|
||||
},
|
||||
{ name: t("navigation.map"), icon: MapIcon, page: "map" },
|
||||
{
|
||||
name: t("navigation.settings"),
|
||||
icon: SettingsIcon,
|
||||
page: "settings",
|
||||
},
|
||||
{
|
||||
name: `${t("navigation.nodes")} (${displayedNodeCount})`,
|
||||
icon: UsersIcon,
|
||||
page: "nodes",
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"relative border-slate-300 dark:border-slate-700",
|
||||
"transition-all duration-300 ease-in-out flex-shrink-0",
|
||||
isCollapsed ? "w-24" : "w-52 lg:w-64",
|
||||
)}
|
||||
>
|
||||
<CollapseToggleButton />
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"h-14 flex mt-2 gap-2 items-center flex-shrink-0 transition-all duration-300 ease-in-out",
|
||||
"border-b-[0.5px] border-slate-300 dark:border-slate-700",
|
||||
isCollapsed && "justify-center px-0",
|
||||
)}
|
||||
>
|
||||
<img src="/logo.svg" alt={t("app.logo")} className="size-10 flex-shrink-0 rounded-xl" />
|
||||
<h2
|
||||
className={cn(
|
||||
"text-xl font-semibold text-gray-800 dark:text-gray-100 whitespace-nowrap",
|
||||
"transition-all duration-300 ease-in-out",
|
||||
isCollapsed ? "opacity-0 max-w-0 invisible ml-0" : "opacity-100 max-w-xs visible ml-2",
|
||||
)}
|
||||
>
|
||||
{t("app.title")}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<SidebarSection label={t("navigation.title")} className="mt-4 px-0">
|
||||
{pages.map((link) => {
|
||||
return (
|
||||
<SidebarButton
|
||||
key={link.name}
|
||||
count={link.count}
|
||||
label={link.name}
|
||||
Icon={link.icon}
|
||||
onClick={() => {
|
||||
if (myNode !== undefined) {
|
||||
navigate({ to: `/${link.page}` });
|
||||
}
|
||||
}}
|
||||
active={link.page === pathname}
|
||||
disabled={myNode === undefined}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</SidebarSection>
|
||||
|
||||
<div className={cn("flex-1 min-h-0", isCollapsed && "overflow-hidden")}>{children}</div>
|
||||
|
||||
<div className=" pt-4 border-t-[0.5px] bg-background-primary border-slate-300 dark:border-slate-700 h-full flex-1">
|
||||
{myNode === undefined ? (
|
||||
<div className="flex flex-col items-center justify-center py-6">
|
||||
<Spinner />
|
||||
<Subtle
|
||||
className={cn(
|
||||
"mt-4 transition-opacity duration-300",
|
||||
isCollapsed ? "opacity-0 invisible" : "opacity-100 visible",
|
||||
)}
|
||||
>
|
||||
{t("loading")}
|
||||
</Subtle>
|
||||
</div>
|
||||
) : (
|
||||
<DeviceInfoPanel
|
||||
isCollapsed={isCollapsed}
|
||||
setCommandPaletteOpen={() => setCommandPaletteOpen(true)}
|
||||
setDialogOpen={() => setDialogOpen("deviceName", true)}
|
||||
user={myNode.user}
|
||||
firmwareVersion={myMetadata?.firmwareVersion ?? t("unknown.notAvailable")}
|
||||
deviceMetrics={{
|
||||
batteryLevel: myNode.deviceMetrics?.batteryLevel,
|
||||
voltage:
|
||||
typeof myNode.deviceMetrics?.voltage === "number"
|
||||
? Math.abs(myNode.deviceMetrics?.voltage)
|
||||
: undefined,
|
||||
}}
|
||||
connectionStatus={activeConnection?.status}
|
||||
connectionName={activeConnection?.name}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user