feat(sync): include avatar in sync snapshot + auto-backup (multi-device)

The profile picture (users.avatar_b64, cosmetic/unencrypted) travelled only
in the manual export. Now it's also in buildSyncSnapshot and the auto-backup
container, so a new device / a restore picks it up.

- Restore is ADDITIVE (mirrors the import path): applyRemoteSnapshot adopts
  remote.avatar_b64 only when the local device has no avatar — never clobbers
  a locally-set picture. No per-avatar timestamp to arbitrate, so changing an
  existing avatar doesn't propagate (cosmetic, accepted).
- +2 merge tests (adopt-when-empty, don't-clobber-when-set). 65/65 green.
- Server /avatar endpoint unchanged (already accepts {avatar_b64}).

Closes the avatar item of CODE_AUDIT §4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-07-08 20:06:31 +01:00
parent dd86b2bd23
commit c58424d58c
5 changed files with 56 additions and 3 deletions
+18
View File
@@ -197,6 +197,9 @@ async function buildSyncSnapshot() {
folders: (state.folders || [])
.filter(f => f && f.name && f.name !== 'All')
.map(f => ({ name: f.name, color: f.color || '', icon: f.icon || '' })),
// Cosmetic, unencrypted — travels so a new device picks up the avatar
// (adopted only if that device has none; see applyRemoteSnapshot).
avatar_b64: state.avatarDataUri || '',
entries: [],
tombstones: [],
};
@@ -355,6 +358,21 @@ async function applyRemoteSnapshot(remote) {
await loadFolders();
}
// Avatar (cosmetic, unencrypted) — adopt the remote one only if this
// device has none, mirroring the import path. Additive so we never fight
// over / clobber a locally-set picture (no per-avatar timestamp to
// arbitrate). A device that already has an avatar keeps it.
if (typeof remote.avatar_b64 === 'string' && remote.avatar_b64 && !state.avatarDataUri) {
try {
await api('/avatar', {
method: 'POST',
headers: authHeaders({ 'Content-Type': 'application/json' }),
body: JSON.stringify({ avatar_b64: remote.avatar_b64 }),
});
state.avatarDataUri = remote.avatar_b64;
} catch (_) {}
}
// Per-entry merge.
for (const r of remote.entries) {
if (!r.uuid) continue;