From f6c94fed1f2c64d6d58e0be1212d48fdca9f2290 Mon Sep 17 00:00:00 2001 From: Randall Hand Date: Fri, 26 Jun 2026 08:48:24 -0400 Subject: [PATCH 1/3] fix(auto-favorite): clarify description to mention prioritized routing (#3774) Update the Auto Favorite section description to read "Automatically favorite eligible nodes for zero-cost hop and prioritized routing." Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01SJPe6J5vKrcbwzt6vCdtrt --- src/components/AutoFavoriteSection.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AutoFavoriteSection.tsx b/src/components/AutoFavoriteSection.tsx index e1027fca1..d53cb4afe 100644 --- a/src/components/AutoFavoriteSection.tsx +++ b/src/components/AutoFavoriteSection.tsx @@ -166,7 +166,7 @@ const AutoFavoriteSection: React.FC = ({ baseUrl }) =>

{t('automation.auto_favorite.description', - 'Automatically favorite eligible nodes for zero-cost hop routing.')}{' '} + 'Automatically favorite eligible nodes for zero-cost hop and prioritized routing.')}{' '} Date: Fri, 26 Jun 2026 09:42:20 -0400 Subject: [PATCH 2/3] fix(auto-favorite): never auto-favorite CLIENT/CLIENT_MUTE, add CLIENT_BASE tip (#3774) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the ZERO_HOP_RELAY_ROLES filter for every eligible local role, not just ROUTER/ROUTER_LATE. Previously a CLIENT_BASE local fell through to an unconditional `return true`, auto-favoriting every 0-hop neighbor including CLIENT and CLIENT_MUTE nodes that never relay. Add a CLIENT_BASE-only info bubble suggesting operators manually favorite their nearby CLIENT/CLIENT_MUTE nodes, since that prioritization is a deliberate per-node choice rather than something Auto Favorite should do. Revert the description copy back to "zero-cost hop routing" — the feature now only covers zero-cost hop relay roles. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01SJPe6J5vKrcbwzt6vCdtrt --- src/components/AutoFavoriteSection.tsx | 22 +++++++++++++++---- src/server/constants/autoFavorite.ts | 12 +++++----- .../meshtasticManager.autoFavorite.test.ts | 12 ++++++++-- 3 files changed, 33 insertions(+), 13 deletions(-) diff --git a/src/components/AutoFavoriteSection.tsx b/src/components/AutoFavoriteSection.tsx index d53cb4afe..273ccc442 100644 --- a/src/components/AutoFavoriteSection.tsx +++ b/src/components/AutoFavoriteSection.tsx @@ -121,9 +121,6 @@ const AutoFavoriteSection: React.FC = ({ baseUrl }) => const getTargetDescription = () => { if (!status?.localNodeRole) return ''; - if (status.localNodeRole === DeviceRole.CLIENT_BASE) { - return t('automation.auto_favorite.target_all', 'all 0-hop nodes'); - } return t('automation.auto_favorite.target_routers', '0-hop Router, Router Late, and Client Base nodes'); }; @@ -166,7 +163,7 @@ const AutoFavoriteSection: React.FC = ({ baseUrl }) =>

{t('automation.auto_favorite.description', - 'Automatically favorite eligible nodes for zero-cost hop and prioritized routing.')}{' '} + 'Automatically favorite eligible nodes for zero-cost hop routing.')}{' '} = ({ baseUrl }) => })}

)} + {status.localNodeRole === DeviceRole.CLIENT_BASE && ( +
+ {t('automation.auto_favorite.client_base_tip', + 'For maximum benefits, you should manually Favorite your local nearby CLIENT and CLIENT_MUTE nodes.')} +
+ )} )} diff --git a/src/server/constants/autoFavorite.ts b/src/server/constants/autoFavorite.ts index a1ce7e447..045396258 100644 --- a/src/server/constants/autoFavorite.ts +++ b/src/server/constants/autoFavorite.ts @@ -7,7 +7,7 @@ export const AUTO_FAVORITE_LOCAL_ROLES: Set = new Set([ DeviceRole.CLIENT_BASE, ]); -/** Roles eligible as zero-cost relay favorites (for ROUTER/ROUTER_LATE local) */ +/** Roles eligible as zero-cost relay favorites (applies to every eligible local role) */ export const ZERO_HOP_RELAY_ROLES: Set = new Set([ DeviceRole.ROUTER, DeviceRole.ROUTER_LATE, @@ -28,8 +28,8 @@ interface AutoFavoriteTarget { * - Target must be 0-hop (hopsAway === 0) * - Target must not have been received via MQTT * - Target must not already be favorited - * - For ROUTER/ROUTER_LATE local: target must also be ROUTER/ROUTER_LATE/CLIENT_BASE - * - For CLIENT_BASE local: any role is eligible + * - Target must be a relay role (ROUTER/ROUTER_LATE/CLIENT_BASE) regardless of the local role. + * CLIENT and CLIENT_MUTE never relay, so they are never auto-favorited (issue #3774). */ export function isAutoFavoriteEligible( localRole: number | undefined | null, @@ -47,10 +47,8 @@ export function isAutoFavoriteEligible( if (target.isFavorite) { return false; } - if (localRole === DeviceRole.ROUTER || localRole === DeviceRole.ROUTER_LATE) { - if (target.role == null || !ZERO_HOP_RELAY_ROLES.has(target.role)) { - return false; - } + if (target.role == null || !ZERO_HOP_RELAY_ROLES.has(target.role)) { + return false; } return true; } diff --git a/src/server/meshtasticManager.autoFavorite.test.ts b/src/server/meshtasticManager.autoFavorite.test.ts index c1dea07c6..dd7ad0d64 100644 --- a/src/server/meshtasticManager.autoFavorite.test.ts +++ b/src/server/meshtasticManager.autoFavorite.test.ts @@ -19,14 +19,22 @@ describe('isAutoFavoriteEligible', () => { expect(isAutoFavoriteEligible(DeviceRole.ROUTER, { hopsAway: 0, role: DeviceRole.CLIENT, isFavorite: false })).toBe(false); }); - it('returns true for 0-hop CLIENT when local is CLIENT_BASE (any role eligible)', () => { - expect(isAutoFavoriteEligible(DeviceRole.CLIENT_BASE, { hopsAway: 0, role: DeviceRole.CLIENT, isFavorite: false })).toBe(true); + it('returns false for 0-hop CLIENT when local is CLIENT_BASE (issue #3774)', () => { + expect(isAutoFavoriteEligible(DeviceRole.CLIENT_BASE, { hopsAway: 0, role: DeviceRole.CLIENT, isFavorite: false })).toBe(false); + }); + + it('returns false for 0-hop CLIENT_MUTE when local is CLIENT_BASE (issue #3774)', () => { + expect(isAutoFavoriteEligible(DeviceRole.CLIENT_BASE, { hopsAway: 0, role: DeviceRole.CLIENT_MUTE, isFavorite: false })).toBe(false); }); it('returns true for 0-hop ROUTER when local is CLIENT_BASE', () => { expect(isAutoFavoriteEligible(DeviceRole.CLIENT_BASE, { hopsAway: 0, role: DeviceRole.ROUTER, isFavorite: false })).toBe(true); }); + it('returns true for 0-hop CLIENT_BASE when local is CLIENT_BASE', () => { + expect(isAutoFavoriteEligible(DeviceRole.CLIENT_BASE, { hopsAway: 0, role: DeviceRole.CLIENT_BASE, isFavorite: false })).toBe(true); + }); + it('returns false for multi-hop node regardless of role', () => { expect(isAutoFavoriteEligible(DeviceRole.ROUTER, { hopsAway: 2, role: DeviceRole.ROUTER, isFavorite: false })).toBe(false); }); From c8afeaca13b6c185c715e70cf18a83fde77b5cc1 Mon Sep 17 00:00:00 2001 From: Randall Hand Date: Fri, 26 Jun 2026 10:11:56 -0400 Subject: [PATCH 3/3] address review: clarify why the two auto-favorite role sets are kept separate Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01SJPe6J5vKrcbwzt6vCdtrt --- src/server/constants/autoFavorite.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/server/constants/autoFavorite.ts b/src/server/constants/autoFavorite.ts index 045396258..c3b7f5de5 100644 --- a/src/server/constants/autoFavorite.ts +++ b/src/server/constants/autoFavorite.ts @@ -1,13 +1,20 @@ import { DeviceRole } from '../../constants/index.js'; -/** Roles that benefit from zero-cost hop favoriting */ +// NOTE: AUTO_FAVORITE_LOCAL_ROLES and ZERO_HOP_RELAY_ROLES currently hold the +// same members but describe two distinct concepts — keep them separate. +// AUTO_FAVORITE_LOCAL_ROLES = which *local* roles may run auto-favorite at all; +// ZERO_HOP_RELAY_ROLES = which *target* roles actually relay and so are worth +// favoriting. They are free to diverge (e.g. a relay role you wouldn't enable +// the feature on, or vice versa), so do not deduplicate them into one set. + +/** Local node roles for which the auto-favorite feature is available */ export const AUTO_FAVORITE_LOCAL_ROLES: Set = new Set([ DeviceRole.ROUTER, DeviceRole.ROUTER_LATE, DeviceRole.CLIENT_BASE, ]); -/** Roles eligible as zero-cost relay favorites (applies to every eligible local role) */ +/** Target roles eligible as zero-cost relay favorites (applies to every eligible local role) */ export const ZERO_HOP_RELAY_ROLES: Set = new Set([ DeviceRole.ROUTER, DeviceRole.ROUTER_LATE,