diff --git a/CLAUDE.md b/CLAUDE.md index 77e68d6..9afe28c 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -270,10 +270,12 @@ La SEULE feature qui sort sur le réseau côté Delphi (HIBP est côté JS). skippe `icon_custom` (même sur "Re-fetch all"), `DELETE /entries/icons/all` filtre `COALESCE(icon_custom,0)=0`. Seul l'utilisateur change/retire une custom, par entrée (boutons Upload/Remove du slideover). `GET /entries` - echoe `icon_custom`. **Non propagé** par import bulk / sync / export - (l'INSERT bulk ne lit pas le flag) → une icône restaurée sur un device - neuf compte comme fetchée ; assumé (les customs sont surtout des sites - self-hosted, que DDG ne re-fetch pas de toute façon). + echoe `icon_custom`. **Propagé** de bout en bout : export manuel, + auto-backup, snapshot de sync l'écrivent ; `encryptImportEntry` le porte + dans le body ; les INSERT **create + bulk-import** le lisent + (`GetValue('icon_custom', False)` → 0 par défaut si absent, ex. + CSV étranger). Round-trip export→import et sync préservent donc la + provenance. Une icône vide (`icon_b64=''`) force toujours `icon_custom=0`. - Bulk : `DELETE /entries/icons/all` purge le cache auto (bouton "Clear cache") — préserve les uploads custom - Render : `entry-avatar` contient `` si diff --git a/delphi-backend/Handlers/PM.Handler.Entries.pas b/delphi-backend/Handlers/PM.Handler.Entries.pas index f9836bc..e153c1b 100644 --- a/delphi-backend/Handlers/PM.Handler.Entries.pas +++ b/delphi-backend/Handlers/PM.Handler.Entries.pas @@ -333,6 +333,7 @@ var LTotpSec, LTotpIv, LKind, LCf, LCfIv, LIcon, LUuid, LTemplateEnc, LTemplateIv, LCreatedAt, LUpdatedAt, LSiteEnc, LSiteIv, LTitleEnc, LTitleIv, LTagsEnc, LTagsIv: string; + LIconCustom: Boolean; LQ: TFDQuery; begin try @@ -371,6 +372,8 @@ begin LCf := LBody.GetValue('custom_fields', ''); LCfIv := LBody.GetValue('custom_fields_iv', ''); LIcon := LBody.GetValue('icon_b64', ''); + // Icon provenance (0 = fetched cache, 1 = user upload). Absent → 0. + LIconCustom := LBody.GetValue('icon_custom', False); LTemplateEnc := LBody.GetValue('template_enc', ''); LTemplateIv := LBody.GetValue('template_iv', ''); // Caller may bring its own UUID (sync restore / import preserving @@ -412,10 +415,10 @@ begin ' site_enc, site_iv, title_enc, title_iv, tags_enc, tags_iv, ' + ' encrypted_password, iv, encryption_method, ' + ' folder, totp_secret, totp_iv, kind, custom_fields, custom_fields_iv,' + - ' icon_b64, template_enc, template_iv, uuid, created_at, updated_at, password_changed_at) ' + + ' icon_b64, icon_custom, template_enc, template_iv, uuid, created_at, updated_at, password_changed_at) ' + 'VALUES (:uid, :uenc, :uiv, :senc, :siv, :tenc, :tiv2, :genc, :giv, ' + ' :e, :i, ''client'', :f, :ts, :tiv, :k, ' + - ' :cf, :cfiv, :ic, :tplenc, :tpliv, :uuid, :c, :c2, :c)'; + ' :cf, :cfiv, :ic, :iconcust, :tplenc, :tpliv, :uuid, :c, :c2, :c)'; LQ.ParamByName('uid').AsInteger := LUserId; // site/username/title/tags cleartext columns dropped — only *_enc written. BindNullable(LQ, 'uenc', LUserEnc); @@ -452,6 +455,8 @@ begin if LCfIv = '' then LQ.ParamByName('cfiv').Clear else LQ.ParamByName('cfiv').Value := LCfIv; LQ.ParamByName('ic').DataType := ftMemo; if LIcon = '' then LQ.ParamByName('ic').Clear else LQ.ParamByName('ic').Value := LIcon; + if (LIcon <> '') and LIconCustom then LQ.ParamByName('iconcust').AsInteger := 1 + else LQ.ParamByName('iconcust').AsInteger := 0; BindNullable(LQ, 'tplenc', LTemplateEnc); BindNullable(LQ, 'tpliv', LTemplateIv); LQ.ParamByName('uuid').AsString := LUuid; @@ -1203,6 +1208,7 @@ var LTotpIv, LNow, LKind, LCf, LCfIv, LIcon, LTemplateEnc, LTemplateIv, LUuid, LCreatedAt, LUpdatedAt, LSiteEnc, LSiteIv, LTitleEnc, LTitleIv, LTagsEnc, LTagsIv: string; + LIconCustom: Boolean; LQ, LTomb: TFDQuery; begin try @@ -1259,10 +1265,10 @@ begin ' site_enc, site_iv, title_enc, title_iv, tags_enc, tags_iv, ' + ' encrypted_password, iv, encryption_method, ' + ' folder, totp_secret, totp_iv, kind, custom_fields, custom_fields_iv,' + - ' icon_b64, template_enc, template_iv, uuid, created_at, updated_at) ' + + ' icon_b64, icon_custom, template_enc, template_iv, uuid, created_at, updated_at) ' + 'VALUES (:uid, :uenc, :uiv, :senc, :siv, :tenc, :tiv2, :genc, :giv, ' + ' :e, :i, ''client'', :f, :ts, :tiv, :k, ' + - ' :cf, :cfiv, :ic, :tplenc, :tpliv, :uuid, :c, :c2)'; + ' :cf, :cfiv, :ic, :iconcust, :tplenc, :tpliv, :uuid, :c, :c2)'; // Declare optional param types ONCE — the prepared statement is // reused across every imported entry, and FireDAC needs the // type set before the first .Clear call would otherwise fail @@ -1310,6 +1316,7 @@ begin LCf := LEntry.GetValue('custom_fields', ''); LCfIv := LEntry.GetValue('custom_fields_iv', ''); LIcon := LEntry.GetValue('icon_b64', ''); + LIconCustom := LEntry.GetValue('icon_custom', False); LTemplateEnc := LEntry.GetValue('template_enc', ''); LTemplateIv := LEntry.GetValue('template_iv', ''); LUuid := Trim(LEntry.GetValue('uuid', '')); @@ -1353,6 +1360,8 @@ begin if LCf = '' then LQ.ParamByName('cf').Clear else LQ.ParamByName('cf').Value := LCf; if LCfIv = '' then LQ.ParamByName('cfiv').Clear else LQ.ParamByName('cfiv').Value := LCfIv; if LIcon = '' then LQ.ParamByName('ic').Clear else LQ.ParamByName('ic').Value := LIcon; + if (LIcon <> '') and LIconCustom then LQ.ParamByName('iconcust').AsInteger := 1 + else LQ.ParamByName('iconcust').AsInteger := 0; BindNullable(LQ, 'tplenc', LTemplateEnc); BindNullable(LQ, 'tpliv', LTemplateIv); LQ.ParamByName('uuid').AsString := LUuid; diff --git a/js/app.backup.js b/js/app.backup.js index 2a82851..b790b47 100644 --- a/js/app.backup.js +++ b/js/app.backup.js @@ -213,6 +213,7 @@ async function runAutoBackupNow(silent) { custom_fields: plainCustom, attachments: attachments, icon_b64: e.icon_b64 || '', + icon_custom: e.icon_custom ? 1 : 0, created_at: e.created_at, updated_at: e.updated_at, }); } diff --git a/js/app.import.js b/js/app.import.js index 73f61c5..2cc90d5 100644 --- a/js/app.import.js +++ b/js/app.import.js @@ -414,6 +414,7 @@ function parseEntriesFromJSON(text) { custom_fields: cf, attachments: atts, icon_b64: String(e.icon_b64 || '').trim(), + icon_custom: e.icon_custom ? 1 : 0, created_at: String(e.created_at || '').trim(), updated_at: String(e.updated_at || '').trim(), }); @@ -487,6 +488,7 @@ async function encryptImportEntry(plain) { custom_fields: cfEnc, custom_fields_iv: cfIv, icon_b64: plain.icon_b64 || '', + icon_custom: plain.icon_custom ? 1 : 0, template: plain.template || '', // Preserve original timestamps on restore — bulk-import falls back // to now only when these are absent (foreign CSV imports). @@ -911,6 +913,7 @@ async function doExport() { custom_fields: plainCustom, attachments: attachments, icon_b64: e.icon_b64 || '', + icon_custom: e.icon_custom ? 1 : 0, created_at: e.created_at, updated_at: e.updated_at, }); diff --git a/js/app.sync.js b/js/app.sync.js index 6dda880..404a762 100644 --- a/js/app.sync.js +++ b/js/app.sync.js @@ -259,6 +259,7 @@ async function buildSyncSnapshot() { custom_fields: plainCustom, attachments, icon_b64: e.icon_b64 || '', + icon_custom: e.icon_custom ? 1 : 0, created_at: e.created_at, updated_at: e.updated_at, });