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:
@@ -92,6 +92,12 @@ type
|
||||
FSavedPlacement: TWindowPlacement;
|
||||
FHasSavedPlacement: Boolean;
|
||||
FOnSystemLock: TProc;
|
||||
// Set TRUE the moment Windows tells us the session is ending
|
||||
// (WM_QUERYENDSESSION / WM_ENDSESSION). FormCloseQuery checks this
|
||||
// to bypass the "minimize to tray" intercept so the form closes
|
||||
// normally and the DB connection is checkpointed instead of being
|
||||
// force-killed (which leaves -shm / -wal files behind).
|
||||
FShutdownPending: Boolean;
|
||||
FOnTrayRestore: TProc;
|
||||
FOnLockRequest: TProc;
|
||||
FOnQuit: TProc;
|
||||
@@ -158,6 +164,9 @@ type
|
||||
property AutofillRegistered: Boolean read FAutofillRegistered;
|
||||
// Fired on main thread when Windows locks the session (WTS_SESSION_LOCK).
|
||||
property OnSystemLock: TProc read FOnSystemLock write FOnSystemLock;
|
||||
// True once WM_QUERYENDSESSION (or WM_ENDSESSION) has been received.
|
||||
// FormCloseQuery uses this to allow normal close during shutdown.
|
||||
property ShutdownPending: Boolean read FShutdownPending;
|
||||
// Fired on main thread when the user clicks the tray icon.
|
||||
property OnTrayRestore: TProc read FOnTrayRestore write FOnTrayRestore;
|
||||
// Fired when the user picks "Lock vault" from the tray menu. Handler
|
||||
@@ -735,6 +744,18 @@ begin
|
||||
if Assigned(FOnSystemLock) then FOnSystemLock();
|
||||
end
|
||||
|
||||
else if (AMsg.Msg = WM_QUERYENDSESSION) or (AMsg.Msg = WM_ENDSESSION) then
|
||||
begin
|
||||
// Windows is logging off / shutting down / restarting. Flip the flag
|
||||
// so FormCloseQuery lets the form actually close instead of
|
||||
// minimizing to tray — otherwise Windows force-kills us after the
|
||||
// shutdown timeout and SQLite's WAL/SHM never get checkpointed.
|
||||
// Return TRUE (do not block shutdown). DefWindowProc returns TRUE
|
||||
// by default for WM_QUERYENDSESSION, so we just don't assign Result.
|
||||
FShutdownPending := True;
|
||||
AMsg.Result := 1;
|
||||
end
|
||||
|
||||
else if (AMsg.Msg <> 0) and (AMsg.Msg = WM_PMShowMessage) then
|
||||
begin
|
||||
// A second instance was launched and PostMessage'd HWND_BROADCAST.
|
||||
|
||||
@@ -314,6 +314,42 @@ begin
|
||||
// Drives the card/table label so notes-with-fields read as "Credit card"
|
||||
// instead of the generic "Encrypted note" placeholder.
|
||||
AddColumnIfMissing('vault_entries', 'template', 'TEXT');
|
||||
// Stable identity that survives export/import + cross-device sync.
|
||||
// SQLite `id` is autoincrement local-only — useless to match the same
|
||||
// logical entry across two installs. Populate existing rows with a
|
||||
// fresh UUID v4 below so the migration is non-destructive.
|
||||
AddColumnIfMissing('vault_entries', 'uuid', 'TEXT');
|
||||
FConn.ExecSQL(
|
||||
'CREATE INDEX IF NOT EXISTS idx_entries_uuid ' +
|
||||
' ON vault_entries(uuid)');
|
||||
// Backfill UUIDs for legacy rows that landed before the column existed.
|
||||
// SQLite has no native uuid() — emit one via hex(randomblob) + manual
|
||||
// dashes (RFC 4122 v4 = 8-4-4-4-12 hex, version nibble forced to 4,
|
||||
// variant nibble high bits 10).
|
||||
FConn.ExecSQL(
|
||||
'UPDATE vault_entries SET uuid = ' +
|
||||
' lower(hex(randomblob(4))) || ''-'' || ' +
|
||||
' lower(hex(randomblob(2))) || ''-4'' || ' +
|
||||
' substr(lower(hex(randomblob(2))), 2) || ''-'' || ' +
|
||||
' substr(''89ab'', 1 + (abs(random()) % 4), 1) || ' +
|
||||
' substr(lower(hex(randomblob(2))), 2) || ''-'' || ' +
|
||||
' lower(hex(randomblob(6))) ' +
|
||||
'WHERE uuid IS NULL OR uuid = ''''');
|
||||
// Tombstones: every hard-delete inserts a row here so the sync engine
|
||||
// can propagate deletes to other devices without leaving deleted
|
||||
// entries to silently reappear at next pull.
|
||||
FConn.ExecSQL(
|
||||
'CREATE TABLE IF NOT EXISTS entry_tombstones (' +
|
||||
' id INTEGER PRIMARY KEY AUTOINCREMENT,' +
|
||||
' user_id INTEGER NOT NULL,' +
|
||||
' uuid TEXT NOT NULL,' +
|
||||
' deleted_at DATETIME DEFAULT CURRENT_TIMESTAMP,' +
|
||||
' FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,' +
|
||||
' UNIQUE(user_id, uuid)' +
|
||||
')');
|
||||
FConn.ExecSQL(
|
||||
'CREATE INDEX IF NOT EXISTS idx_tombstones_user ' +
|
||||
' ON entry_tombstones(user_id, deleted_at DESC)');
|
||||
// Per-folder customisation. NULL = no override → JS uses the default
|
||||
// accent + i-folder symbol.
|
||||
AddColumnIfMissing('folders', 'color', 'TEXT');
|
||||
|
||||
Reference in New Issue
Block a user