feat(companion): contact expiry + prune, and scope-list follow-up fixes

Settings > Contacts gains "Expire" (Off/7d/30d/90d) and a "Prune now" action
that counts first and asks before removing anything. A contact with no
advert/update within the threshold is eligible; favourites are always kept.
Thresholds and their labels live in one NodePrefs table so the age shown and
the age enforced can't drift. SCHEMA_SENTINEL -> 0xC0DE002C (sizeof unchanged
at 2824, confirmed on native and a real WioTrackerL1 build).

Also fixes four bugs in the scope list from 89c02eea:

- removeScope() saved /scopes1 but never savePrefs(), so the ch_scope_idx[]
  and repeat_extra_scope_mask fix-ups it makes were lost on reboot, leaving
  shifted entries against unshifted indices.
- CMD_SET_DEFAULT_FLOOD_SCOPE wrote the legacy fields directly instead of
  going through setPrimaryScope(), so the app's default-scope setting had
  nothing reading it once sends resolved through the list.
- The reverse direction was stale too: an on-device "Set as default" never
  refreshed default_scope_name/key, so CMD_GET_DEFAULT_FLOOD_SCOPE reported
  a scope the device had stopped using. New syncLegacyDefaultScope().
- Upgrading from the old single Scope field set the default (so DMs kept it)
  but left every channel unscoped, since "*" means unscoped, not "inherit".
  loadScopeList() now reports when it migrated and begin() seeds the channels
  that already exist, leaving empty slots alone.

"Set default" -> "Set as default", and setting it now says what it governs
("Default: DMs + relay") rather than leaving the [default] marker to imply
more than it does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Jakub
2026-09-12 21:55:46 +02:00
co-authored by Claude Opus 5
parent 89c02eea27
commit 1d5dbe2f9a
8 changed files with 273 additions and 28 deletions
+27 -1
View File
@@ -498,6 +498,13 @@ struct NodePrefs { // persisted to file
// memset and an older prefs file (no bytes here at all) mean "on", which is
// the default -- a positive flag would read back as off for every upgrader.
uint8_t fav_sort_off; // 0 = favourites first in every list (default), 1 = natural order
// Settings > Contacts > "Expire" + "Prune now". Index into
// contactExpiryDays()/contactExpiryLabel() below (0=Off/never, 1=7d, 2=30d,
// 3=90d) -- a contact whose ContactInfo::lastmod is older than this is
// eligible for the manual Prune-now sweep. Favourites are always exempt
// regardless of age. On-disk position is the struct's append-only tail (see
// the serialization tripwire below), same as fav_sort_off above.
uint8_t contact_expiry_idx; // 0 = off (default)
// ── Advert ─────────────────────────────────────────────────────────────
uint8_t advert_loc_policy;
@@ -579,6 +586,21 @@ struct NodePrefs { // persisted to file
// tuning only — not persisted.
static const uint16_t TRAIL_AUTOPAUSE_MOVE_M = 15;
// Contact-expiry thresholds (days) for contact_expiry_idx. Single source of
// truth for both the Settings > Contacts "Expire" row's label and the age
// MyMesh::countStaleContacts()/pruneStaleContacts() actually applies, so the
// number the user picks and the one enforced can't drift apart. 0 = off,
// which is also the clamp target for any out-of-range saved index.
static const uint8_t CONTACT_EXPIRY_COUNT = 4;
static uint16_t contactExpiryDays(uint8_t idx) {
static const uint16_t D[CONTACT_EXPIRY_COUNT] = { 0, 7, 30, 90 };
return D[idx < CONTACT_EXPIRY_COUNT ? idx : 0];
}
static const char* contactExpiryLabel(uint8_t idx) {
static const char* L[CONTACT_EXPIRY_COUNT] = { "Off", "7d", "30d", "90d" };
return L[idx < CONTACT_EXPIRY_COUNT ? idx : 0];
}
// Tail sentinel written at the end of /new_prefs. Bump the low byte when
// adding/removing/reordering fields in DataStore::savePrefs/loadPrefsInt so
// older saves are detected on load and skipped (zero-init defaults kept).
@@ -588,7 +610,7 @@ struct NodePrefs { // persisted to file
// repeat_* fields) instead of at the tail, which shifted every field after
// them by 25 bytes when loading an older file. Never released, but a dev
// build wrote it, so the number must not be reused for anything else.
static const uint32_t SCHEMA_SENTINEL = 0xC0DE002B;
static const uint32_t SCHEMA_SENTINEL = 0xC0DE002C;
// Bit-index for each home page. Used by page_order (entries store bit+1) and
// by home_pages_mask. Single source of truth — both HomeScreen::pageBit/bitToPage
@@ -733,6 +755,10 @@ struct NodePrefs { // persisted to file
// earlier bump -- confirmed via a real sim_companion_radio (native) build
// and a real WioTrackerL1_companion_solo_dual (nRF52/ARM) build, sizeof
// 2824 on both.
// contact_expiry_idx (0xC0DE002C) landed in existing padding elsewhere in
// the struct -- confirmed via a real sim_companion_radio (native) build and a
// real WioTrackerL1_companion_solo_dual (nRF52/ARM) build, sizeof unchanged
// at 2824 on both.
static_assert(sizeof(NodePrefs) == 2824,
"NodePrefs layout changed — sync DataStore save/load + clamp, bump "
"SCHEMA_SENTINEL, then update this size (see steps above).");