Commit Graph

88 Commits

Author SHA1 Message Date
r-zakarya c74b708b39 fix(import): count the trash when deduplicating by uuid
The map was built from state.entries only, so a container entry whose uuid existed just in the trash looked fresh and inserted a second row sharing that uuid - the identity the sync engine relies on. Same blind spot as the resurrection bug in applyRemoteSnapshot.

Also documents F9 and warns that emptying the trash is the wrong way to clean duplicate uuids: the tombstone would kill the live twin on the other device.
2026-08-07 17:35:40 +01:00
r-zakarya 125cb11756 docs(tests): mark I6 validated — every scenario now passes 2026-08-07 14:55:47 +01:00
r-zakarya afe47259d7 fix(unlock): focus the username first when it is empty, on the quick-search path too
The autofill hotkey already picked whichever field still needed filling; quick search forced the password, so a first unlock or one without a remembered username landed the cursor in the second field.
2026-08-07 14:53:36 +01:00
r-zakarya 476d6660ad fix(hotkeys): a locked vault must not hand the foreground back
Both locked-vault paths cancelled the autofill with the default refocus, which returns the foreground to the captured target - right after Delphi had raised our window. Ctrl+Shift+Q flashed for a frame then Notepad took over; Ctrl+Shift+L moved the window twice because focusApp had to take it back.
2026-08-07 14:48:12 +01:00
r-zakarya d242f09c53 docs(tests): mark F1/F3/F4 validated, section F complete 2026-08-07 12:50:14 +01:00
r-zakarya c3c6bce406 fix(sync): a trashed entry is no longer resurrected as a duplicate
The trash is a soft delete: no tombstone is written and the rows are absent from state.entries, so the merge read their uuids as missing locally and POSTed them back as fresh live entries. Trashing 30 entries then syncing produced 30 live duplicates alongside the trashed originals - two rows sharing one uuid.

applyRemoteSnapshot now loads the trash and skips any uuid found there. Emptying the trash is what writes the tombstone that propagates the delete.
2026-08-07 12:38:07 +01:00
r-zakarya b9b098fe12 fix(editor): fire an input event when the generator fills the password
Assigning .value fires nothing, and the code called soDirtyCheck by hand - so the strength bar, which listens to input, stayed frozen on the previous password. Dispatching the event covers every listener on the field instead of one named handler.
2026-08-07 12:03:42 +01:00
r-zakarya 485dd3631e docs(tests): E5 validated by code inspection, distinguish it from E9
Web storage and process memory are different artefacts; the dump scan says nothing about the former. Also flags the removeItem('cryptoKey') cleanup as deliberate, not dead code.
2026-08-07 11:46:18 +01:00
r-zakarya c1e5d14eac docs(tests): mark H2-H6 validated 2026-08-07 11:02:05 +01:00
r-zakarya af1157f3b0 docs(tests): note that the trash is silently excluded from every backup path 2026-08-03 00:41:15 +01:00
r-zakarya 5236aa8b98 fix(busy): gate the document-level shortcuts too
inert removes elements from focus and hit testing, but shortcuts are listened for on the document and never traverse an element. Ctrl+A still selected every entry under the overlay, Ctrl+K would open the palette behind it, j/k moved the cursor - and Delete ran a batch delete while a sync was writing to the same rows.
2026-08-03 00:04:56 +01:00
r-zakarya a36164174e docs(tests): record the four secret-consuming paths and how to test them
Includes the authenticated WsgiDAV recipe (Basic only, digest off) and the change-event race that can produce a misleading 401.
2026-08-03 00:02:27 +01:00
r-zakarya c8518c806d docs(tests): mark F2 validated, note that the file existing proves nothing
A backup encrypted with an empty password looks perfectly valid on disk; only a successful import proves the stored password reached the encryption.
2026-08-02 23:55:04 +01:00
r-zakarya f8e78f6940 docs(tests): add E9, the renderer memory check that caught the backup password
Records the staging, how to pick the renderer (the GPU child is bigger and holds no JavaScript, so scanning it proves nothing), the soState validity marker, and the measured before/after.
2026-08-02 23:37:07 +01:00
r-zakarya 3c14836b15 docs(tests): add D6 for the sticky header spacing 2026-08-02 00:11:06 +01:00
r-zakarya ecaa226354 docs(tests): record that D5 needs checking in all three view modes 2026-08-02 00:06:09 +01:00
r-zakarya 8c633b69de docs(tests): mark D5 validated 2026-08-02 00:03:02 +01:00
r-zakarya a12f3a2c91 fix(list): toggle a checkbox in place instead of rebuilding the grid
renderGrid replaces every card, so the scroll container jumped back to the top: ticking a card near the bottom of a long list threw the user back to the first row. Only the affected card and the selection bar are updated now.
2026-08-01 23:59:53 +01:00
r-zakarya cb0a366e52 docs(tests): mark B19 validated 2026-08-01 23:53:08 +01:00
r-zakarya 69b0e84f63 fix(editor): whitelist the bar controls, not the whole bars
content-header and pagination were exempt from click-away in full, so their large empty areas were dead: clicking beside the title or next to the page buttons left the editor open. Only the actual controls are exempt now.
2026-08-01 23:48:54 +01:00
r-zakarya dc7b18736e fix(dnd): drop targets only light up for our own entry drags
They accepted any drag then looked for ids in text/plain: dragging text selected in a cell produced NaN, filtered out, so the target invited a drop that could do nothing. Entry drags now carry application/x-pm-entries, mirroring the folder MIME, and all four targets check it before accepting.
2026-08-01 23:32:47 +01:00
r-zakarya 2027d9b820 docs(tests): mark G6 validated 2026-08-01 23:25:00 +01:00
r-zakarya 69a62994b4 fix(views): drop the selection whenever the view changes
Checked ids belong to the view they were made in. Carried into the trash they were LIVE entries while the trash bar offers Delete forever, so a purge could destroy rows that had never been trashed. The checkbox-less views were the visible symptom of the same problem; one rule replaces the special case.
2026-08-01 23:14:31 +01:00
r-zakarya 9e5c5945f9 fix(views): clear the selection when entering a view without checkboxes
The batch bar survived into vault health, the authenticator grid and the audit log, none of which render checkboxes - it kept offering Delete, Move to folder and Add tag on entries the user could no longer see selected.
2026-08-01 22:49:42 +01:00
r-zakarya 961d77534f fix(hibp): move the progress pill off the sidebar and abort the scan on toggle-off
The pill was pinned to the left edge and covered the navigation, hiding Trash. It also kept counting after Breach check disabled, the workers carrying on with one network round-trip per entry.
2026-08-01 20:12:09 +01:00
r-zakarya e80703d2c4 fix(health): invalidate the cache when HIBP results change
The dashboard reads its pwned column from state.hibpResults but its cache was computed before the scan finished, so enabling the breach check from the health view showed 0 pwned while the entry cards already carried the flag. Disabling had the mirror problem: results cleared, dashboard still counting them.
2026-08-01 19:49:07 +01:00
r-zakarya 2436825ed6 docs(tests): G2 is unreachable by design, document the health score steps
G3 invalidates the cache on every mutation, so a stale health list can no longer be staged. Also records why the score moves in steps of 10 and the per-category caps that can make early fixes invisible.
2026-08-01 19:29:40 +01:00
r-zakarya 254bbc3bca docs(tests): mark B14 validated 2026-08-01 19:19:18 +01:00
r-zakarya eaadc06daa docs(tests): mark B18 validated 2026-08-01 19:15:01 +01:00
r-zakarya dca98dde9b fix(busy): swallow the scrolling keys while a blocking operation runs
inert removes the controls from focus but not the document scroll: Space, PageUp/Down, Home/End and the arrows scroll without any focused element, so the page still moved under the overlay.
2026-08-01 19:12:36 +01:00
r-zakarya e07488e8b4 fix(busy): make the page inert so the keyboard cannot reach controls behind the overlay
The overlay covers the page, which stops the mouse but not Tab and Space. The inert attribute removes the subtree from focus, hit-testing and the accessibility tree in one go - applied to every body-level sibling except the overlay, and always cleared in hideBusy.
2026-08-01 19:09:16 +01:00
r-zakarya 4a78dc2e90 fix(hotkeys): find the native dialog by enumerating the thread windows
GetLastActivePopup returns the form itself - the common Save dialog is not an owned popup - so the code that raises it never ran. It does live on our thread with the standard #32770 class, which EnumThreadWindows finds regardless of its owner.

Logging confirmed two things worth keeping: WM_HOTKEY is dispatched during the dialog modal loop, and IsWindowEnabled detects the state correctly.
2026-08-01 19:03:03 +01:00
r-zakarya d6701a8994 fix(hotkeys): attach to the foreground thread to focus the buried dialog
The autofill helper attaches the caller to the TARGET thread, but the dialog runs on our own thread, so the attach was a no-op and Windows kept refusing the foreground change - the dialog rose without focus. Attaching to the current foreground thread instead is what grants the call.
2026-08-01 18:53:20 +01:00
r-zakarya c9c1e98470 fix(hotkeys): focus the buried dialog and say why the shortcut did nothing
SetForegroundWindow is refused when the hotkey comes from another application, so the dialog was raised but never focused; ForceForegroundWindow is now exposed by PM.Bridge for that. The explanation goes through a tray balloon because our window is disabled and behind the dialog, where no toast would be seen.
2026-08-01 18:49:03 +01:00
r-zakarya 1c0423680d fix(hotkeys): never touch the window while a native modal owns it
Ctrl+Shift+Q and Ctrl+Shift+A call RestoreWindowFromTray before JS can refuse, so a JS guard is too late by construction. Restoring the owner of a Save As dialog stranded it behind everything and the app looked frozen while it was only disabled.

NativeModalActive tests IsWindowEnabled - Windows disables the owner for the lifetime of the modal - and brings the dialog back to front via GetLastActivePopup instead of merely refusing.
2026-08-01 18:40:45 +01:00
r-zakarya 42d2d6fda5 fix(autofill): refusing a request must not move any window
The busy guard cancelled through the path that hands the foreground back to the captured target. During an export that pushed the native Save dialog behind the other application, leaving the app disabled by a modal the user could no longer find. refocus=0 separates refusing a request from ending a user interaction.
2026-08-01 18:30:00 +01:00
r-zakarya 03cc7adc25 fix(autofill): refuse a fill while a blocking operation runs
Guarded in autofillHandleRequest, upstream of the single-match versus picker split, so it covers Ctrl+Shift+L/P/T and both outcomes: the picker would open behind the overlay, and a single-match fill would minimise our window mid-operation. Cancels the request so Delphi drops the captured target HWND.
2026-08-01 18:04:35 +01:00
r-zakarya be629b2bcf fix(hotkeys): block the editor and quick search while a blocking operation runs
Global hotkeys come from Win32, outside the window, so the busy overlay intercepts nothing: Ctrl+Shift+A opened the editor behind it, unreachable. Guarded in openSlideOver and openQuickSearchModal rather than at each trigger - every caller routes through them, and no save path raises the overlay.
2026-08-01 18:02:20 +01:00
r-zakarya 6bb64832ef fix(editor): guard the second Esc path against the busy overlay too
There are two Escape handlers: a capture-phase one for the editor and the global one-surface-per-keystroke chain. Only the first was guarded, so export - which reaches the editor through the global chain - still popped the discard-confirm under the overlay.
2026-08-01 17:35:04 +01:00
r-zakarya 5d10935ae2 fix(editor): ignore Esc while a blocking operation owns the screen
The discard-confirm opened underneath the busy overlay, which sits at z-index 200 - blurred and half-clickable. Worse than the display: it let the user arbitrate closing the editor while a sync or a rotation was writing to the same entry.
2026-08-01 16:49:09 +01:00
r-zakarya d8a1728371 docs(tests): mark A13b validated 2026-08-01 16:45:48 +01:00
r-zakarya bd86899c2b feat(entries): offer to save the editor content as a new entry when the row is gone
A 404 on save meant the work had to be copied out by hand. The POST mints a fresh uuid, so this is a new entry rather than a resurrection and cannot fight the tombstone the delete left behind. Gated on status 404 AND the server message: the HTTP layer answers 404 to any request it refuses, which the status alone would misread.
2026-08-01 16:41:11 +01:00
r-zakarya 5eb279ff31 fix(entries): 404 on PUT when the row no longer exists
The UPDATE matched nothing but the handler still answered 200 Updated, so the editor reported Saved for an edit that went nowhere - the entry had been deleted by a sync or a purge after the editor opened it. Silent data loss; the client already surfaces a failed save as an error toast.
2026-08-01 16:20:45 +01:00
r-zakarya 7158664b0d docs(tests): replace a stray image reference in A12b with its check mark 2026-08-01 16:06:11 +01:00
r-zakarya 2579264285 docs(tests): F6 fully validated, sync round-trip included 2026-08-01 15:56:14 +01:00
r-zakarya 9b32ae407e docs(tests): add F7 for the sync last-write-wins arbitration
Records the two traps that invalidate the test: any PUT rebumps updated_at, undoing a manual rollback, and created_at plays no part in the arbitration.
2026-08-01 15:31:32 +01:00
r-zakarya 9dc9f625d6 fix(entries): size the multiline field from its content instead of a fixed height
A manual resize was lost on reopen: the row is rebuilt on every open, and remembering the height would not survive a restart either since localStorage is wiped when the ephemeral port changes the origin. Deriving it from the content removes the state entirely - the field is always the right size.
2026-07-31 18:31:26 +01:00
r-zakarya 2cab20eb76 docs(tests): add F6 for the multiline Notes field 2026-07-28 23:21:21 +01:00
r-zakarya 67107fba37 fix(unlock): persist unlockMode in DPAPI so a cold start still asks for the PIN
unlockMode lived only in localStorage, wiped on every launch since the ephemeral port changes the origin, and in settings_json, which needs a session to read. The auth screen therefore always fell back to the master password on a cold start even though the PIN blob was there. Mirror it to a DPAPI pref and read it back in showAuth, like rememberedUsername.
2026-07-28 22:50:28 +01:00
r-zakarya 52af21f917 docs(tests): mark B1-B13 validated 2026-07-28 20:07:49 +01:00