Files
JakubandClaude Opus 5 1d5dbe2f9a 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>
2026-09-12 21:55:46 +02:00

86 lines
3.9 KiB
C++

#pragma once
#include <helpers/IdentityStore.h>
#include <helpers/ContactInfo.h>
#include <helpers/ChannelDetails.h>
#include "NodePrefs.h"
#include "ScopeList.h"
class DataStoreHost {
public:
virtual bool onContactLoaded(const ContactInfo& contact) =0;
virtual bool getContactForSave(uint32_t idx, ContactInfo& contact) =0;
virtual bool onChannelLoaded(uint8_t channel_idx, const ChannelDetails& ch) =0;
virtual bool getChannelForSave(uint8_t channel_idx, ChannelDetails& ch) =0;
};
class DataStore {
FILESYSTEM* _fs;
FILESYSTEM* _fsExtra;
mesh::RTCClock* _clock;
IdentityStore identity_store;
void loadPrefsInt(const char *filename, NodePrefs& prefs, double& node_lat, double& node_lon);
#if defined(NRF52_PLATFORM) || defined(STM32_PLATFORM)
void checkAdvBlobFile();
#endif
public:
DataStore(FILESYSTEM& fs, mesh::RTCClock& clock);
DataStore(FILESYSTEM& fs, FILESYSTEM& fsExtra, mesh::RTCClock& clock);
void begin();
bool formatFileSystem();
FILESYSTEM* getPrimaryFS() const { return _fs; }
FILESYSTEM* getSecondaryFS() const { return _fsExtra; }
bool loadMainIdentity(mesh::LocalIdentity &identity);
bool saveMainIdentity(const mesh::LocalIdentity &identity);
void loadPrefs(NodePrefs& prefs, double& node_lat, double& node_lon);
void savePrefs(const NodePrefs& prefs, double node_lat, double node_lon);
void loadContacts(DataStoreHost* host);
void saveContacts(DataStoreHost* host, bool (*filter)(const ContactInfo& c) = NULL);
// Returns true if a channels file (current or legacy) existed and was
// loaded -- false only on a genuinely fresh device with neither file, so
// the caller knows whether it's safe to seed default channels (see
// MyMesh::begin()'s addChannel("Public", ...) -- seeding unconditionally
// would resurrect a channel the user had deliberately deleted, since a
// deleted slot is simply absent from the file, not written as empty).
bool loadChannels(DataStoreHost* host);
void saveChannels(DataStoreHost* host);
// /scopes1: the shared named-scope list (see ScopeList.h). `prefs` is only
// read, for a one-time migration of a pre-existing single
// default_scope_name/key into list entry 1 -- the file is authoritative
// once it exists.
//
// Returns true ONLY when that legacy migration just ran, i.e. this boot is
// the first on a device that had a Scope configured the old way. The caller
// uses that to seed the channels that already exist with the migrated entry
// (see MyMesh::begin) -- channels carry their own scope now, so without the
// seed an upgrader's channel traffic would silently drop to unscoped even
// though their DMs kept the old scope. Returns false when /scopes1 was
// already there, and on a genuinely fresh device with nothing to migrate
// (list left empty, default_idx 0 == "*").
bool loadScopeList(ScopeList& list, const NodePrefs& prefs);
void saveScopeList(const ScopeList& list);
void migrateToSecondaryFS();
uint8_t getBlobByKey(const uint8_t key[], int key_len, uint8_t dest_buf[]);
bool putBlobByKey(const uint8_t key[], int key_len, const uint8_t src_buf[], uint8_t len);
bool deleteBlobByKey(const uint8_t key[], int key_len);
File openRead(const char* filename);
File openRead(FILESYSTEM* fs, const char* filename);
File openWrite(const char* filename);
// Atomically replace final_path with a fully-written temp file (see
// openWrite()). Use for small custom records that want the same crash-safety
// as contacts/channels: write everything to a .tmp, then commit. Returns
// false if the swap fails (the previous good file is left untouched).
bool commitFile(const char* tmp_path, const char* final_path);
bool removeFile(const char* filename);
bool removeFile(FILESYSTEM* fs, const char* filename);
uint32_t getStorageUsedKb() const;
uint32_t getStorageTotalKb() const;
void saveRTCTime();
void restoreRTCTime();
private:
FILESYSTEM* _getContactsChannelsFS() const { if (_fsExtra) return _fsExtra; return _fs;};
};