Commit Graph

574 Commits

Author SHA1 Message Date
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 e919a79522 docs(claude): document the multiline Notes field and the custom-field row 2026-08-01 14:00:45 +01:00
r-zakarya 4936aa78ca refactor(entries): drop the cyan secret marker on the reveal eye
The eye only renders on a secret field, so its presence already carries the state.
2026-08-01 12:47:59 +01:00
r-zakarya a8c1a88ec4 refactor(entries): drop the inline copy button, the row menu carries it
Row is down to label | value (eye inside) | menu.
2026-08-01 12:05:44 +01:00
r-zakarya e5758f984c fix(entries): keep the reveal eye clear of the multiline scrollbar
scrollbar-gutter reserves the track whether or not it is showing, so the eye keeps the same position once the content passes the auto-grow cap.
2026-08-01 12:04:46 +01:00
r-zakarya a2cd08c676 feat(entries): move the reveal eye inside the custom-field value
Same placement as the TOTP field, which buys back a column on a dense row: label | value (eye inside) | copy | menu. Pinned top-right on a multiline field so it stays clear of the resize grip.
2026-08-01 11:57:56 +01:00
r-zakarya c71ea643e7 feat(entries): add Copy value to the custom-field row menu
The inline copy button stays: copy is the frequent action and burying it costs a click every time. Unlike the old lock-versus-eye pair, two ways to copy are not confusable.
2026-08-01 11:55:29 +01:00
r-zakarya a0a069c7f6 refactor(entries): fold the secret toggle into a per-field row menu
The lock button sat next to the eye and both masked the value, so they read as the same control. The eye now carries the state - cyan ring when the field is stored as secret - and flipping the flag moved into a row menu alongside Remove field, one deliberate step away from the everyday reveal. The row drops from six columns to five.

The menu reuses the card kebab classes, so closing it on an outside click is already handled.
2026-08-01 11:47:25 +01:00
r-zakarya 6705a8b478 fix(entries): toggle the secret flag in place instead of rebuilding every row
rerender() wipes the whole custom-fields list, which reads as the panel flashing and also drops focus and the auto-sized height of a multiline field.
2026-08-01 11:06:59 +01:00
r-zakarya 4b7f5ff599 docs(entries): spell out the difference between the reveal eye and the secret lock
Both toggles mask the value, so they read as duplicates. Tooltips now say which one is temporary and which one changes what is stored.
2026-08-01 10:34:44 +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 d8643b96d1 feat(entries): multiline Notes field for any entry, masked by default
The note body of a note entry is the encrypted_password column, so logins had no way to carry notes. Rather than a dedicated encrypted column and the seven call sites that come with it, this rides on custom_fields, which rotation, duplicate, export/import and sync already carry.

Both the open-time normalisation and the save-time rebuild drop any key they do not name, so multiline is carried explicitly in each - otherwise the field silently reopens single-line.
2026-07-28 23:06:39 +01:00
r-zakarya 415dc6c8ba docs(claude): record that pre-login settings must live in DPAPI prefs 2026-07-28 22:51:00 +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
r-zakarya 83a0e08df8 docs(tests): fix B6, a trashed entry can never be open in the editor
openTrashActions is an empty stub and the trash kebab has no Edit item, so the scenario as written was not reachable. Rewritten around what is: the purge must leave an unrelated open editor alone, plus the one remote path that can actually trash an entry while it is being edited.
2026-07-28 19:54:37 +01:00
r-zakarya 4499c8c570 docs(tests): mark H11/H12 validated 2026-07-28 19:44:13 +01:00
r-zakarya 80711272c4 fix(tray): a -tray launch does take the foreground, so give it back
FMX shows its taskbar proxy before we hide it, so the launching console was left inactive. GStartupForeground, captured in the unit initialization before any window of ours exists, holds it. A Windows-startup launch is unaffected: the foreground is a shell window there, which the capture filters out.
2026-07-28 19:38:02 +01:00
r-zakarya 7c08a69bb2 docs(tests): mark H13 validated, Ctrl+Shift+P confirmed on Reddit 2026-07-28 15:50:25 +01:00
r-zakarya 16577a22be docs(tests): record why a full fill cannot work on the Reddit login form 2026-07-28 15:48:49 +01:00
r-zakarya 38ba208455 fix(autofill): stop sending Ctrl+A after the Tab in a full fill
Tabbing into an input already selects its whole content, so the second clear was redundant. Where Tab lands on something that is not editable - a link, a reveal button, the document, as on the Reddit login form - Ctrl+A selected the entire page instead and the password went nowhere.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-28 15:32:25 +01:00
r-zakarya 40e2054496 docs(tests): add H11/H12 for the foreground-handback guards 2026-07-27 23:17:52 +01:00
r-zakarya 2a1da8913b fix(tray): do not un-minimise the application we hand the foreground back to
ForceForegroundWindow restores an iconic target, so closing to the tray would pop back open a window the user had deliberately minimised. Skip the handback in that case and leave the choice to Windows. A -tray launch also skips it: it never took the foreground from anyone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 23:14:12 +01:00
r-zakarya 33d8df3dee fix(tray): give the foreground back to the application we took it from
Hiding a window hands the foreground to nobody, so we stayed foreground while
invisible. The next hotkey then captured our own window as its target and typed
the password into it, and Ctrl+Shift+A opened a New entry with an empty title —
the guard correctly refusing to prefill with "Password Manager".

Track foreground changes with an EVENT_SYSTEM_FOREGROUND hook and keep the last
window that is neither ours nor a shell one, then reactivate it just before
hiding. Sampling on demand cannot work: by then the user's application is two
foreground changes behind (their app, the taskbar they clicked, us), and
reactivating after the hide is silently refused since only the current
foreground owner may give focus away.

Autofill paths pass AReturnForeground=False — they already placed the
foreground on the window they filled.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 23:10:22 +01:00
r-zakarya a9e56c52b5 fix(autofill): send the window back to the tray when the picker is dismissed
Cancelling left the window on the desktop, though it had only been restored to
ask which entry to fill. Quick-search already hides itself on Esc; this makes
the picker symmetric. Applies to all three hotkeys — the capture and the hide
both sit on the shared BridgeAutofillRequest / autofill/cancel paths, so full,
password-only and TOTP behave identically.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 21:11:43 +01:00
r-zakarya 3c84bdca49 fix(autofill): return to the tray after picking in the multi-match picker
Only the quick-search path captured whether the window was tray-hidden and fed
it back as hide_after; JS never sends that parameter from the autofill picker,
so Ctrl+Shift+L pulled the window out of the tray and left it on the desktop.

Capture it in BridgeAutofillRequest instead, when the hotkey fires — by execute
time the picker has already restored the window and the state is gone. Same
rule as quick-search: tray-hidden only, a taskbar-minimised window is not sent
to the tray it never came from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 21:03:57 +01:00
r-zakarya e98acabbbd revert(winstate): drop the parked restored-size rect, it left a stub window
Widening rcNormalPosition while the window was down did remove the restore
flash, but SetWindowPlacement left a small stub frame in a corner of the screen
whenever the window was maximised — during the minimise animation and equally
once the window was already iconic. A permanent visible artefact is worse than
an ugly animation, so this goes back to the validated behaviour: correct final
state, flash still present.

The flash itself is an FMX bug, reproducible in an empty FMX app. Rationale and
the five rejected approaches are recorded in TEST_SCENARIOS.md I1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 20:51:28 +01:00
r-zakarya 17e817f8ab fix(winstate): park the restored-size rect so the FMX restore has nothing to grow
FMX drops the maximised state when Windows minimises the form externally —
reproducible in an empty FMX app. Windows then picks rcNormalPosition as the
restore target before it animates, so every reactive correction measured (queued,
inline in MINIMIZEEND, inside the restore's own WM_SIZE) landed after the frame
was already growing: the state ended correct and the flash remained.

Widen rcNormalPosition to the monitor work area while the window is down instead.
FMX still restores to normal, except normal is now full-screen, so nothing grows;
ReleaseNormalRect writes the real rectangle back once the window is maximised
again, keeping the user's own restored size intact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 20:13:52 +01:00
r-zakarya b52cdc2d0b fix(winstate): apply the wanted window state inline, killing the restore flash
The MINIMIZEEND correction was posted through ForceQueue. Instrumenting the
hook showed the window state identical at the hook and one message-loop turn
later, so the queue guarded no race and only bought the normal-sized frame the
user saw. ApplyWantedWindowState now runs inline; the deferred pass stays as an
idempotent net.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 19:39:23 +01:00
r-zakarya 99b68c3e67 docs(startup): correct the rationale for the deferred server start
The comment justified deferring btnStartClick by the taskbar-proxy exposure
window, which the .dpr fix has since removed. Kept as-is per decision — it
works and reaching the tray sooner is desirable — but the comment now says
that, so nobody "fixes" it later against a premise that no longer holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 14:18:19 +01:00
r-zakarya ca23b88582 docs: section L — Windows startup (--tray)
L1 hidden start with no taskbar button, L2 the button coming back on restore,
L3 the normal launch (validated). Records why every fix placed inside the form
was powerless: the FMX proxy is shown before FormCreate is reached.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 14:12:31 +01:00
r-zakarya 8d03b74b6a chore: drop the -tray debugger run parameter from the project file
Committed by accident while testing the tray launch. Left in, every F9 from
the IDE would start hidden in the tray — the exact confusion the parameter is
worth warning about. It is a local debugging preference, not project config.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 14:09:29 +01:00