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,134 @@
|
||||
import { Button } from "@components/UI/Button.tsx";
|
||||
import useLocalStorage from "@core/hooks/useLocalStorage.ts";
|
||||
import { useToast } from "@core/hooks/useToast.ts";
|
||||
import { useCallback, useEffect, useRef } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface UseBackupReminderOptions {
|
||||
reminderInDays?: number;
|
||||
message: string;
|
||||
onAccept?: () => void | Promise<void>;
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface ReminderState {
|
||||
expires: string;
|
||||
}
|
||||
|
||||
const TOAST_APPEAR_DELAY = 10_000; // 10 seconds
|
||||
const TOAST_DURATION = 30_000; // 30 seconds
|
||||
const REMINDER_DAYS_ONE_WEEK = 7;
|
||||
const REMINDER_DAYS_ONE_YEAR = 365;
|
||||
const REMINDER_DAYS_FOREVER = 3650;
|
||||
const STORAGE_KEY = "key_backup_reminder";
|
||||
|
||||
function isReminderExpired(expires?: string): boolean {
|
||||
if (!expires) {
|
||||
return true;
|
||||
}
|
||||
const expiryDate = new Date(expires);
|
||||
if (Number.isNaN(expiryDate.getTime())) {
|
||||
return true; // Invalid date passed
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
return now.getTime() >= expiryDate.getTime();
|
||||
}
|
||||
|
||||
export function useBackupReminder({
|
||||
enabled,
|
||||
message,
|
||||
onAccept = () => {},
|
||||
reminderInDays = REMINDER_DAYS_ONE_WEEK,
|
||||
}: UseBackupReminderOptions) {
|
||||
const { t } = useTranslation("dialog");
|
||||
|
||||
const { toast } = useToast();
|
||||
const toastShownRef = useRef(false);
|
||||
const [reminderState, setReminderState] = useLocalStorage<ReminderState | null>(
|
||||
STORAGE_KEY,
|
||||
null,
|
||||
);
|
||||
|
||||
const setReminderExpiry = useCallback(
|
||||
(days: number) => {
|
||||
const expiryDate = new Date();
|
||||
expiryDate.setDate(expiryDate.getDate() + days);
|
||||
setReminderState({ expires: expiryDate.toISOString() });
|
||||
},
|
||||
[setReminderState],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!enabled || toastShownRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!isReminderExpired(reminderState?.expires)) {
|
||||
return;
|
||||
}
|
||||
|
||||
toastShownRef.current = true;
|
||||
|
||||
const { dismiss } = toast({
|
||||
title: t("pkiBackupReminder.title"),
|
||||
duration: TOAST_DURATION,
|
||||
delay: TOAST_APPEAR_DELAY,
|
||||
description: message,
|
||||
action: (
|
||||
<div className="flex flex-col gap-2">
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="p-1"
|
||||
onClick={() => {
|
||||
dismiss();
|
||||
setReminderExpiry(reminderInDays);
|
||||
}}
|
||||
>
|
||||
{t("pkiBackupReminder.remindLaterPrefix")} {reminderInDays}{" "}
|
||||
{reminderInDays > 1
|
||||
? t("common:unit.day.plural").toLowerCase()
|
||||
: t("common:unit.day.one").toLowerCase()}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
className="p-1"
|
||||
onClick={() => {
|
||||
dismiss();
|
||||
setReminderExpiry(REMINDER_DAYS_FOREVER);
|
||||
}}
|
||||
>
|
||||
{t("pkiBackupReminder.remindNever")}
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
variant="default"
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
onAccept();
|
||||
dismiss();
|
||||
setReminderExpiry(REMINDER_DAYS_ONE_YEAR);
|
||||
}}
|
||||
>
|
||||
{t("pkiBackupReminder.backupNow")}
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
return () => dismiss();
|
||||
}, [
|
||||
enabled,
|
||||
message,
|
||||
onAccept,
|
||||
reminderInDays,
|
||||
reminderState?.expires,
|
||||
setReminderExpiry,
|
||||
t,
|
||||
toast,
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user