diff --git a/src/components/AutoFavoriteSection.tsx b/src/components/AutoFavoriteSection.tsx index e1027fca1..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'); }; @@ -238,6 +235,23 @@ const AutoFavoriteSection: React.FC = ({ 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..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 (for ROUTER/ROUTER_LATE local) */ +/** 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, @@ -28,8 +35,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 +54,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); });