feat: WebDAV sync + batch DnD + clean shutdown + center-modal UX bundle

- Sync (WebDAV, auto-merge): UUID + tombstones foundations (server +
  JS), THTTPClient bridge cmds (get/put/test), runSyncNow engine with
  pull/merge/push flow, Settings UI, pre-sync backup option. Test
  connection now treats 404 as OK (snapshot not yet created) and 401/
  403 as auth failure with dedicated toast.
- Batch drag-drop: cards + table rows carry checked-set ids (CSV) when
  dragged from an active selection; folder + trash drop handlers parse
  and apply in batch via new moveEntriesToFolder helper that preserves
  TOTP / custom_fields / kind in the full PUT payload.
- Clean shutdown: WM_QUERYENDSESSION / WM_ENDSESSION captured in the
  bridge message-only window; FormCloseQuery bypasses the tray-minimize
  intercept on system shutdown / restart / logoff so FireDAC closes the
  SQLite WAL cleanly instead of leaving -shm / -wal residue after a
  force-kill.
- Center-mode modal: blur+dim backdrop via body::before pseudo-element
  in editor-position=center, swallows clicks below the panel so the
  existing outside-click handlers reliably dismiss the slideover /
  settings panel.
- Batch bar state fixes: state.checked cleared before render in
  moveEntriesToFolder, emptyTrash, and per-card restoreEntry /
  permanentDelete / deleteEntry so the action bar disappears once the
  selection is fully processed.
- Save-then-discard duplicate fix: soState reset to null before
  openSlideOver re-opens the freshly saved entry, otherwise the dirty
  check fired on the soState.id=null → newId switch and a Cancel left
  the form in new-entry mode (second Save → POST duplicate).
- TEST_SYNC.md: end-to-end checklist for validating the WebDAV sync
  with 2 real instances.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
r-zakarya
2026-06-30 00:32:12 +01:00
parent b00da43ab0
commit 6869b7c692
10 changed files with 1132 additions and 25 deletions
+47
View File
@@ -454,6 +454,53 @@ hotkeys autofill, etc. **Device-only** (localStorage seulement) :
`quickUnlockEnabled` (DPAPI lié au compte Windows), `autofillEnabled`
(toggle hotkey Win32), `rememberedUsername` (auth screen autofill local).
## Sync (WebDAV, auto-merge)
Multi-device sync via a user-hosted WebDAV server (Nextcloud, ownCloud,
Apache mod_dav). Auto-merge strategy: last-write-wins per entry on
`updated_at`, tombstones propagate hard-deletes. No conflict UI — solo
personal use rarely produces simultaneous edits.
Foundations :
- `vault_entries.uuid` (TEXT, indexed) — stable cross-device identity.
Migration backfills existing rows via `hex(randomblob)` → RFC 4122 v4.
- `entry_tombstones (user_id, uuid, deleted_at)` — UNIQUE(user_id, uuid),
written on hard-delete (`DELETE permanent=1`, trash empty, auto-purge).
- GET `/entries` returns uuid ; POST/bulk-import accept it (mint fresh
if absent) ; PUT keeps it immutable.
- GET `/entries/tombstones` lists local tombstones.
- POST `/entries/tombstones {uuids:[...]}` adds tombstones + hard-deletes
any local rows matching those uuids (idempotent via INSERT OR IGNORE).
Transport ([UMainForm.pas](delphi-backend/UMainForm.pas)) :
- `cmd://webdav/get|put|test?reqId=&url=&user=&pwd=[&data=]` → async via
`THTTPClient` (WinHTTP under the hood, no OpenSSL DLLs required).
Basic auth, 10s connect / 30s response timeout. Callback
`Bridge.onWebdavResult(reqId, status, payload)`.
Settings : all config in DPAPI prefs (`syncEnabled`, `syncUrl`,
`syncUser`, `syncPwd`, `syncEncPwd`, `syncPreBackup`, `syncLast`).
**`syncEncPwd` MUST be the same on every device** — it's the secret
that encrypts the WebDAV-stored snapshot. User sets it once per
device, never transmitted.
`runSyncNow()` flow :
1. (Optional) Write `vault-presync-yyyymmdd-HHMMSS.json` to the
auto-backup folder if enabled.
2. `webdav/get` → 404 = first sync, treat as empty remote.
3. Decrypt with `syncEncPwd` (reuses `encryptExportPayload` container).
4. POST remote tombstones → server hard-deletes local matches.
5. Folders : add missing ones additively (don't touch existing).
6. Entries : for each remote uuid → not in local = POST keeping uuid +
restore attachments ; both sides have it = compare `updated_at`,
PUT if remote newer.
7. `loadEntries()` + `buildSyncSnapshot()` for the post-merge state.
8. `webdav/put` push the merged snapshot.
9. Toast `X added · Y updated · Z deleted`.
Sensitive actions (export, change master pw, recovery code…) still
require master pw via `askReauth` — sync never substitutes.
## PIN unlock
Optional shortcut unlock with a 412 digit PIN, complementary to Quick