diff --git a/lxc/sparse-cone.txt b/lxc/sparse-cone.txt index 2225a0778..c9aa78307 100644 --- a/lxc/sparse-cone.txt +++ b/lxc/sparse-cone.txt @@ -1,30 +1,30 @@ -# lxc/sparse-cone.txt -# --------------------------------------------------------------------------- -# Sparse-checkout cone list for the MeshMonitor LXC template build. -# -# WHAT THIS FILE CONTROLS -# build-lxc-template.sh reads this file at build time and passes the listed -# directories to git sparse-checkout set. Only these directories, plus -# all root-level files (package.json, tsconfig*.json, vite.config.ts, etc.), -# which cone mode always includes automatically, are materialized inside -# /opt/meshmonitor in the container rootfs. Everything else (docs/, desktop/, -# .github/, etc.) is never fetched, keeping the template lean (~8MB .git vs -# ~51MB for a full clone). -# -# MAINTENANCE -# If you add a new top-level directory that is required at runtime, add it -# here or the LXC template will silently omit those files on the next build. -# After updating: rebuild the template and confirm meshmonitor-update works. -# -# FOR AI ASSISTANTS -# This is the single source of truth for LXC template directory inclusion. -# Update this file in the same commit whenever you add a runtime-required -# top-level directory. See CLAUDE.md "LXC Template Build" for the full rule. -# --------------------------------------------------------------------------- - -src -public -docker -protobufs -scripts +# lxc/sparse-cone.txt +# --------------------------------------------------------------------------- +# Sparse-checkout cone list for the MeshMonitor LXC template build. +# +# WHAT THIS FILE CONTROLS +# build-lxc-template.sh reads this file at build time and passes the listed +# directories to git sparse-checkout set. Only these directories, plus +# all root-level files (package.json, tsconfig*.json, vite.config.ts, etc.), +# which cone mode always includes automatically, are materialized inside +# /opt/meshmonitor in the container rootfs. Everything else (docs/, desktop/, +# .github/, etc.) is never fetched, keeping the template lean (~8MB .git vs +# ~51MB for a full clone). +# +# MAINTENANCE +# If you add a new top-level directory that is required at runtime, add it +# here or the LXC template will silently omit those files on the next build. +# After updating: rebuild the template and confirm meshmonitor-update works. +# +# FOR AI ASSISTANTS +# This is the single source of truth for LXC template directory inclusion. +# Update this file in the same commit whenever you add a runtime-required +# top-level directory. See CLAUDE.md "LXC Template Build" for the full rule. +# --------------------------------------------------------------------------- + +src +public +docker +protobufs +scripts lxc \ No newline at end of file diff --git a/src/server/meshcoreManager.contactPersistence.test.ts b/src/server/meshcoreManager.contactPersistence.test.ts index 2a862fbb6..80bcb1e24 100644 --- a/src/server/meshcoreManager.contactPersistence.test.ts +++ b/src/server/meshcoreManager.contactPersistence.test.ts @@ -284,4 +284,48 @@ describe('MeshCoreManager contact persistence (issue #3092)', () => { advType: MeshCoreDeviceType.REPEATER, }); }); + + it('keeps the known name when a later advert has an empty adv_name (issue #3756)', async () => { + const manager = new MeshCoreManager('src-a'); + + // First advert carries the real name. + dispatchBridgeEvent(manager, { + event_type: 'contact_advertised', + data: { + public_key: REPEATER_PUBKEY, + adv_name: 'MyRepeater', + adv_type: MeshCoreDeviceType.REPEATER, + }, + }); + await Promise.resolve(); + await Promise.resolve(); + + // Second advert (e.g. a zero-hop repeater) arrives with an empty name. + // With `??` this would overwrite the stored name with ""; `||` keeps it. + dispatchBridgeEvent(manager, { + event_type: 'contact_advertised', + data: { + public_key: REPEATER_PUBKEY, + adv_name: '', + adv_type: MeshCoreDeviceType.REPEATER, + }, + }); + await Promise.resolve(); + await Promise.resolve(); + + // In-memory contact retains the original name. + expect(manager.getContact(REPEATER_PUBKEY)).toMatchObject({ + publicKey: REPEATER_PUBKEY, + advName: 'MyRepeater', + }); + + // The DB write from the empty-name advert also preserves the name. + expect(upsertNode).toHaveBeenLastCalledWith( + expect.objectContaining({ + publicKey: REPEATER_PUBKEY, + name: 'MyRepeater', + }), + 'src-a', + ); + }); }); diff --git a/src/server/meshcoreManager.ts b/src/server/meshcoreManager.ts index e2d1d8adc..108c3f571 100644 --- a/src/server/meshcoreManager.ts +++ b/src/server/meshcoreManager.ts @@ -1109,7 +1109,10 @@ class MeshCoreManager extends EventEmitter { const updated: MeshCoreContact = { ...existing, publicKey, - advName: data.adv_name ?? existing.advName, + // `||` not `??`: zero-hop repeaters (and some firmware builds) emit + // `contact_advertised` with adv_name === "", which `??` would pass + // through and overwrite the known name with an empty string (#3756). + advName: data.adv_name || existing.advName, advType: data.adv_type ?? existing.advType, lastAdvert: data.last_advert ?? existing.lastAdvert, latitude: data.latitude ?? existing.latitude, @@ -1470,7 +1473,9 @@ class MeshCoreManager extends EventEmitter { await databaseService.meshcore.upsertNode( { publicKey: contact.publicKey, - name: contact.advName ?? contact.name ?? null, + // `||` not `??` so an empty advName falls back to name rather than + // persisting "" into the node row (#3756). + name: contact.advName || contact.name || null, advType: contact.advType ?? null, latitude: atNullIsland ? null : (contact.latitude ?? null), longitude: atNullIsland ? null : (contact.longitude ?? null),