From 4ffbd63893ac21b47c0fbbd8d2983681f5095ec5 Mon Sep 17 00:00:00 2001 From: r-zakarya <82443831+r-zakarya@users.noreply.github.com> Date: Sun, 5 Jul 2026 15:55:23 +0100 Subject: [PATCH] fix(sync): bump updated_at on set-icon + folder-delete reassignment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both UPDATEs mutated a synced column without touching updated_at, so the change rode in the sync snapshot but other devices skipped it (last-write- wins saw "not newer"). Now both SET updated_at = datetime('now') (UTC). - POST /entries/{id}/icon (PM.Handler.Entries) - folder delete → entries reassigned to 'All' (PM.Handler.Folders) accessed_at stays exempt (read timestamp, not synced); bulk "clear all icons" stays exempt (device-local favicon cache purge). Invariant documented in CLAUDE.md. Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 8 ++++++++ delphi-backend/Handlers/PM.Handler.Entries.pas | 5 ++++- delphi-backend/Handlers/PM.Handler.Folders.pas | 5 ++++- 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index f51dc0d..620ea98 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -868,6 +868,14 @@ le vide ET vide `FPendingURL` → écran noir permanent sur cold-start lent. nécessite `AttachThreadInput` pour cross-process. - **`updated_at`** est bumpé à chaque PUT entry → sort par défaut = `name` asc pour que modifier une entry ne change pas sa position. +- **Toute mutation `vault_entries` qui doit se synchroniser DOIT bumper + `updated_at`** : la sync est last-write-wins sur `updated_at`, donc un + `UPDATE` qui ne le touche pas voyage bien dans le snapshot mais est ignoré + par les autres devices (« pas plus récent »). Corrigé pour set-icône + (`POST /entries/{id}/icon`) et réassignation-sur-delete-folder + (`PM.Handler.Folders`). `accessed_at` est **exempté** exprès (timestamp de + lecture, non synced). Le bulk « clear all icons » ne bump pas non plus — + assumé device-local (purge de cache favicon). - **Timestamps = UTC partout** : tout `created_at`/`updated_at`/`deleted_at` écrit côté Delphi passe par `NowUTC`/`NowUTCStr` (`PM.Database`) — JAMAIS `FormatDateTime(..., Now)` (heure locale). SQLite `CURRENT_TIMESTAMP` / diff --git a/delphi-backend/Handlers/PM.Handler.Entries.pas b/delphi-backend/Handlers/PM.Handler.Entries.pas index 04828da..d1dc5cb 100644 --- a/delphi-backend/Handlers/PM.Handler.Entries.pas +++ b/delphi-backend/Handlers/PM.Handler.Entries.pas @@ -883,7 +883,10 @@ begin try LQ.Connection := DB.Connection; LQ.SQL.Text := - 'UPDATE vault_entries SET icon_b64 = :ic ' + + // Bump updated_at (UTC) so the icon change wins last-write-wins on + // sync — without it the new icon_b64 rides in the snapshot but other + // devices skip it (timestamp unchanged → "not newer"). + 'UPDATE vault_entries SET icon_b64 = :ic, updated_at = datetime(''now'') ' + 'WHERE id=:id AND user_id=:uid'; LQ.ParamByName('ic').DataType := ftMemo; // long text → ftMemo on SQLite if LIcon = '' then LQ.ParamByName('ic').Clear diff --git a/delphi-backend/Handlers/PM.Handler.Folders.pas b/delphi-backend/Handlers/PM.Handler.Folders.pas index d176253..da7d33e 100644 --- a/delphi-backend/Handlers/PM.Handler.Folders.pas +++ b/delphi-backend/Handlers/PM.Handler.Folders.pas @@ -350,7 +350,10 @@ begin try LQ.Connection := DB.Connection; LQ.SQL.Text := - 'UPDATE vault_entries SET folder = ''All'' ' + + // Bump updated_at (UTC) so the folder reassignment propagates on sync + // (last-write-wins) — otherwise other devices keep the entries in the + // just-deleted folder because the timestamp didn't move. + 'UPDATE vault_entries SET folder = ''All'', updated_at = datetime(''now'') ' + 'WHERE user_id = :uid AND folder = :name'; LQ.ParamByName('uid').AsInteger := LUserId; LQ.ParamByName('name').AsString := LName;