Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions src/components/AutoFavoriteSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,6 @@ const AutoFavoriteSection: React.FC<AutoFavoriteSectionProps> = ({ 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');
};

Expand Down Expand Up @@ -238,6 +235,23 @@ const AutoFavoriteSection: React.FC<AutoFavoriteSectionProps> = ({ baseUrl }) =>
})}
</div>
)}
{status.localNodeRole === DeviceRole.CLIENT_BASE && (
<div style={{
marginLeft: '1.75rem',
marginBottom: '1rem',
padding: '0.75rem 1rem',
background: 'var(--ctp-surface0)',
border: '1px solid var(--ctp-sapphire)',
borderLeft: '4px solid var(--ctp-sapphire)',
borderRadius: '6px',
color: 'var(--ctp-sapphire)',
fontSize: '13px',
lineHeight: '1.5',
}}>
{t('automation.auto_favorite.client_base_tip',
'For maximum benefits, you should manually Favorite your local nearby CLIENT and CLIENT_MUTE nodes.')}
</div>
)}
</>
)}

Expand Down
21 changes: 13 additions & 8 deletions src/server/constants/autoFavorite.ts
Original file line number Diff line number Diff line change
@@ -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<number> = 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<number> = new Set([
DeviceRole.ROUTER,
DeviceRole.ROUTER_LATE,
Expand All @@ -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,
Expand All @@ -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;
}
Expand Down
12 changes: 10 additions & 2 deletions src/server/meshtasticManager.autoFavorite.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
Expand Down
Loading