Commit Graph

260 Commits

Author SHA1 Message Date
r-zakarya d101192e22 fix(recovery): don't commit the code until the user saves it
Generating a recovery code POSTed /recovery-key/setup — which deletes any
existing recovery row and inserts the new one — BEFORE showing the code. So
cancelling the "save this code" modal still erased the old code and activated
a new one the user never saw: a silent path to lockout.

Generate + wrap locally, show the code, and only POST setup after "I saved
it". Cancel now writes nothing — the account is left exactly as it was, old
code still valid.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 00:26:32 +01:00
r-zakarya 9b7d04ee46 fix(reauth): wrong master pw re-prompts inline instead of closing
Setting a PIN, enabling Quick Unlock, and generating a recovery key all
closed the reauth modal and showed a toast on the first wrong master
password — no retry. Export already looped inline; the others didn't.

Extracted that loop into reauthLoop(message): re-prompts with an inline
error + attempt count (5 tries), honours the 429 lockout, returns the
verified password or null. All four sensitive-action flows now share it, so
a mistyped master password keeps the modal up to try again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 23:52:05 +01:00
r-zakarya b542106c76 security: stop caching the raw vault key in sessionStorage
A memory dump of the WebView2 renderer (vault unlocked) showed the AES vault
key sitting in sessionStorage as a clean base64 string — a plaintext copy of
the master secret, trivially grep-able, present the whole session.

persistCryptoKey/restoreCryptoKey existed only to survive a page reload, but
the shell has no reload path (F5/Ctrl+R blocked, every navigation is a cmd://
bridge call), so the branch that consumed it was unreachable. Removed both
functions, their 7 call sites, and collapsed the init restore branch into the
plain cold-start path (Quick Unlock → PIN → master-pw, all already there).

Also scrub the derived key bytes from the heap (keyBytes.fill(0)) once the
CryptoKey and verifier are built — the CryptoKey keeps its own copy.

Not fixable here, documented: the key stays extractable:true (Quick Unlock,
PIN, recovery and master-pw change all exportKey it), and decrypted password
strings still linger in the V8/Blink heap after lock — immutable strings the
GC never zeroes, inherent to a JS/WebView2 password manager.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 23:40:56 +01:00
r-zakarya 5f807bcf23 fix(editor): warn before overwriting an entry changed elsewhere (A11)
The editor re-ships the payload it loaded on open, so a sync or import that
rewrote the same entry meanwhile was silently clobbered by Save — the last
hole editorAbsorbChange doesn't cover, since those paths rewrite every field
including the ciphertext.

Save now compares the live entry against the baseline soState already keeps
and asks before overwriting. Ciphertext comparison, so no decryption; changes
the editor absorbed read as equal (editorAbsorbChange updates `original`
too), so local tag/folder actions never trip it; the post-save reload
re-baselines, so a second save is clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 22:44:31 +01:00
r-zakarya 04bc809c71 fix(editor): refresh the icon preview on outside icon writes
A10: the editor's icon preview is painted by a closure nothing outside can
reach, so a favicon fetched from Settings (or an import overwrite) updated
the cards and left the open editor showing the old initials. soIconField
publishes its painter; saveEntryIcon — the choke point for every icon write
— routes through editorAbsorbChange like tag/folder already do.

Also whitelists .toast and #busyOverlay in the editor's click-outside: both
can sit under the cursor during a long operation, and clicking one counted
as clicking away.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 21:19:21 +01:00
r-zakarya 47c29bd04e fix(esc): reauth / picker / quick-search were missing from the Esc chain
Three static modals had no entry in the fallback Esc handler, so Esc over
them fell through and closed the editor behind (or did nothing when no
editor was open — reauth and the autofill picker could only be dismissed by
mouse).

Also scopes the stray-modal catch added in a95becd to [data-dynamic]. As
written it matched ANY open .modal, so an Esc over reauth / quick-search /
the autofill picker would have removed those static nodes from the DOM for
good, breaking the feature until restart. Only the runtime-built folder
modal carries the marker.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 21:17:53 +01:00
r-zakarya a95becd582 fix(editor): Esc over the folder modal no longer closes the editor
The folder modal is built dynamically with no id in the fallback Esc
handler's modal list, so Esc fell straight through to the slideover close
and shut the editor behind it. The capture handler already bails on any
open .modal; mirror that in the fallback by closing a stray .modal before
reaching the slideover branch. Covers every dynamically-built modal, not
just this one.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 19:17:45 +01:00
r-zakarya 6f4363bde6 fix(batch): move the selection bar into the title row
Selecting an entry inserted the batch bar as a block above the grid, pushing
every row down ~60px — the list jumped under the cursor mid-click. The bar
now renders inside .content-header, to the right of "All items", and the
header reserves its height (min-height) so appearing and disappearing shifts
nothing at all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-23 15:59:53 +01:00
r-zakarya 9ea26be422 docs: manual test scenarios for the concurrent-write / editor bugs
Covers the four classes found this session (stale editor payload, editor
dismissed without the discard gate, missing withEncryptedMeta, view-blind
UI) with the surrounding cases, not just the ones reported.

Also fixes one more instance found while auditing: deleting a folder
reassigns its entries to All server-side, but an editor open on one of them
kept pointing at the deleted folder and re-created it on Save.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 23:51:12 +01:00
r-zakarya d146fbe796 fix(editor): outside tag/folder changes land in the open editor
Tagging or moving an entry from the batch bar / kebab / drag while that same
entry sat open in the editor was lost on the next Save: the editor still held
the payload it loaded before the change and re-shipped it wholesale.

editorAbsorbChange(id, {tag|folder}) mirrors the change into soState AND
soState.original (so the dirty check doesn't fire on an edit the user didn't
type), called from all five write paths: batchAddTag, batchMoveToFolder,
addTagToEntry, moveEntryToFolder, moveEntriesToFolder.

Two bugs found next door:
- moveEntriesToFolder (multi-drag) PUT without withEncryptedMeta. The
  cleartext columns are gone server-side, so it shipped fields the server
  can't store and blanked title/site/username/tags at rest.
- The kebab menu offered Pin/Edit/Duplicate/Move to trash on TRASHED rows —
  the table view reaches it in trash, where only Restore / Delete forever
  make sense.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 23:44:10 +01:00
r-zakarya e1ddc65f85 fix(editor): batch-bar clicks no longer dismiss the editor
The click-outside handler whitelists chrome (topbar, sidebar, pagination…)
but not .batch-bar, so Delete / Clear / Move to folder / Add tag counted as
an outside click and ran requestCloseSlideOver before their own handler.
Whitelist it, plus .content-header which carries buttons of its own
(Empty trash). Those actions now decide for themselves what happens to the
editor via closeEditorForDeleted.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 23:33:52 +01:00
r-zakarya 3dbce780d2 fix(delete): one editor gate for every delete path
Batch Delete had the bug the single ✕ just lost: it never touched the open
editor, so an entry could be trashed while its editor stayed open on it,
unsaved edits and all. Same hole in batch purge, permanent delete and empty
trash.

closeEditorForDeleted(ids, alreadyConfirmed) now owns the rule for all five
call sites: close the editor only when it holds one of the deleted ids, and
show the discard prompt only when no delete modal was shown (one modal per
action; cancelling it deletes nothing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 23:22:14 +01:00
r-zakarya 4b9ffaab62 fix(trash): warn about unsaved edits when delete asks nothing
With askBeforeDelete off, clicking ✕ on the entry being edited trashed it
and closed the editor with no prompt whatsoever — a misclick cost the edits
silently. That path now runs the discard gate (no-op unless dirty). When
askBeforeDelete is on, its own confirm still stands alone: one modal.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 23:09:15 +01:00
r-zakarya 4c2287fd35 fix(trash): don't close the editor when another entry is trashed
Clicking the ✕ on any row called closeSlideOver() unconditionally, so an
open editor with unsaved edits on a DIFFERENT entry vanished without the
discard prompt. Close it only when it holds the entry being trashed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 23:03:45 +01:00
r-zakarya 3f07c1217b feat(notes): eye toggle to mask the note body in the editor
The note textarea showed its plaintext to anyone glancing at the screen,
unlike every other secret in the editor. An eye button on the NOTE label
masks it with -webkit-text-security (a textarea has no type="password").

The button IS the setting: it flips state.maskNotes, which is synced via
settings_json, so the choice sticks across entries and devices — no extra
Settings row for a one-bit preference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 20:16:06 +01:00
r-zakarya a0b6c5c67a style(list): note rows get the same 110px track as password rows
The note placeholder was still flex:1, so note rows' copy button sat at the
far edge while password rows' sat at 110px. Same fixed track for both, full
label on hover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 11:53:25 +01:00
r-zakarya 506230f138 fix(list): shrink the password pill and tooltip truncated names
The list-view rule set `width: 110px`, but the base .entry-pw is `flex: 1`
(basis 0), so width was ignored and the pill stretched the full row width —
dragging the copy/open buttons to the far edge. `flex: 0 0 110px` pins it,
and the actions land right after it.

Names that ellipsise in the fixed-width title column (list) and the name
cell (table) now carry a title attr, so the full name shows on hover.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 11:50:52 +01:00
r-zakarya 24d74df2ab feat(layout): stick the title row too, and chain the sticky offsets
"All items · N items" scrolled away like the pager did. It now sticks at
the scrollport top (padding-top/negative margin cancel .content's padding
so nothing shows through the gap), and the three things below it offset
by --hdr-h: batch bar, pagination, and the table header (--hdr-h + --pag-h).
Both vars are measured on .content in renderEntries.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 11:31:52 +01:00
r-zakarya 60cf26b259 feat(pagination): keep the pager visible while scrolling
The page buttons sat at the top of the grid and scrolled away, so paging
through a long list meant scrolling back up. Sticky at the scrollport top
with an opaque background; the table header now sticks below it via
--pag-h (measured after append, so a wrapped pager still lines up).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 11:22:50 +01:00
r-zakarya a1e028bfc5 fix(table): right-align the column-picker header cell
The gear hugged the left edge of the (width:100%) actions column because the
header th, unlike the body cells, has no col-actions class / text-align:right.
Set textAlign right on the actions header cell so the picker sits in the
top-right corner.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-22 06:51:52 +01:00
r-zakarya 4dfd908a9a style(table): move the column picker into the table header corner
The show/hide-columns gear only makes sense in table view but sat in the
global toolbar (needing applyColsWrapVisibility to hide it elsewhere). Build
it inside the actions-column header cell in renderTable instead, with inline
handlers (recreated each render, so no dependence on once-wired listeners on
a static node). Removed #colsWrap from the toolbar + the now-obsolete
applyColsWrapVisibility and its callers; kept the menu id so
renderTableColsMenu still populates it, and a live-queried outside-click close.
Added .cols-wrap CSS (was an inline position:relative on the old node).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 20:00:56 +01:00
r-zakarya 0d40328a24 feat(hotkey): make the new-entry hotkey configurable + fix tray title capture
Part A — Ctrl+Shift+A (new entry from window title) is now editable like the
other 3 global hotkeys, following the same pattern end to end: state.newEntryHotkey
(synced), a capture button in Settings > Autofill, dup cross-check, reset-to-
defaults, autofillPushHotkeys, and Bridge.setAutofillHotkeys carrying ne_mods/
ne_vk. Delphi gets SetNewEntryHotkey + stores the live combo (FNewEntryHotkeyMods/
Vk) so ExecuteAutofill's post-fill re-register uses the user's combo instead of
resetting to Ctrl+Shift+A. UMainForm parses ne_* unconditionally (works with
autofill off).

Part B — new-entry from the tray pre-filled the title with "Password Manager":
GetForegroundWindow resolved to our own hidden window. Only capture the title
when the foreground window belongs to another process; otherwise pass '' (blank).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 19:49:48 +01:00
r-zakarya 5799407f55 fix(settings): sync the view-mode toolbar button to the server value on load
viewMode is a synced setting. On launch localStorage is wiped (ephemeral port
= new origin) so state.viewMode defaults to 'cards' and applyViewMode()
highlights the cards button. loadServerSettings then set state.viewMode='table'
and re-rendered the content (table) but never re-synced the .view-btn
highlight or the column-picker — so table content showed under a cards-active
toolbar. Re-apply both in the visual-settings block, alongside theme/sidebar.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 19:24:09 +01:00
r-zakarya 71f45eb8e1 fix(kdf): disable the data-loss KDF auto-migration path (audit 6c-1)
/migrate-kdf changed the AES key (100k->600k) but only re-encrypted
encrypted_password, leaving totp_secret/custom_fields/*_enc + attachments
under the old key = permanent data loss. Both live accounts are already at
600k so it never fired, but the path was armed and auto-triggered by the
server's login signal.

Removed the two triggers (doLogin + unlock) and the now-dead runKdfMigration.
Legacy <600k accounts upgrade safely via Change Master Password instead
(re-encrypts everything + moves to Argon2id). The Delphi endpoint is left
inert: without the client orchestrating the key swap it can't corrupt
anything, so no untested server change. Closes 6c-4 too (unreachable now).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 05:20:07 +01:00
r-zakarya 64a843d23a feat(palette): add the sidebar Tools + Settings as commands
Generator, Authenticator, TOTP generator, Vault health, Audit log, Import,
Export and Settings are now reachable from Ctrl+K. View-based tools reuse
the sidebar buttons' click handlers so their cache invalidation rides along.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 05:22:19 +01:00
r-zakarya a090c64081 feat(security): "Lock vault when Windows locks or sleeps" toggle (default ON)
Explicit opt-out for users without Quick Unlock who don't want to retype the
master password after every sleep. Default ON = exact historical behavior
(lock on WTS lock/suspend, with the documented Quick Unlock exemption —
DPAPI already gates access via the Windows account). Synced setting,
Settings > Security.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 05:17:16 +01:00
r-zakarya 8555661823 fix(esc): palette closes before the info modals it opens over
Cheatsheet open -> Ctrl+K -> Esc closed the cheatsheet UNDER the palette:
the priority chain tested cheatsheet/history before cmdPalette. Palette now
sits right after confirmModal (still the absolute top) in the one-surface-
per-keystroke chain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 05:03:31 +01:00
r-zakarya 1751f0534f fix(slideover): opening the editor closes Settings (was hidden underneath)
Ctrl+Shift+A with Settings open created the entry panel BELOW the Settings
panel — invisible. openSlideOver now closes Settings first (root fix: covers
every editor-open path, not just the hotkey), placed after the discard guard
so cancelling keeps Settings. The inverse — opening Settings over the editor
— stays as-is by design (Esc: Settings first, editor second).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 04:57:20 +01:00
r-zakarya 829056f1fa fix(import): normalize timestamps to the DB format at the import door
vault_entries uses SQLite's space-separated UTC format everywhere, and both
sorting and sync last-write-wins compare the strings lexically — so a foreign
JSON import carrying strict-ISO 'T'/millis/Z/offset timestamps would slot in
with a different format and subtly break ordering and merge arbitration.
normalizeImportTimestamp converts any ISO-ish variant to 'YYYY-MM-DD
HH:MM:SS' UTC (bare strings treated as UTC, garbage -> '' = server stamps
now). +1 unit test (69 total).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-13 00:13:41 +01:00
r-zakarya 8cc1599434 fix(slideover): double-click on Save no longer creates a duplicate entry
soSave had no re-entrancy guard: a second click while the first run awaited
encryption/POST ran the whole save again -> two POSTs, two entries. Wrapped
in a soSaving latch (same class as the sync-button guard); body moved to
soSaveInner so every early validation return releases the latch via finally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 23:57:43 +01:00
r-zakarya 51f72e560a fix(ctxmenu): Esc with the context menu open closes only the menu
Same Esc fall-through class: the menu's Esc handler was bubble-phase and
didn't stop the keystroke, so the slideover capture handler fired first and
popped the discard prompt while the menu also hid. Capture + stopPropagation,
gated on the menu being visible; registered before the slideover handler
(installCustomContextMenu runs at the top of init) so ordering is guaranteed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:48:32 +01:00
r-zakarya bf606493bd fix: three more bugs of the same classes (Esc fall-through, chord, placement)
Hunted the classes behind the recent quick-search fixes across the codebase:

- Esc priority: the command palette is .cmd-palette (not .modal), so the
  slideover's capture-phase Esc handler didn't see it and popped the
  discard-confirm UNDER the open palette. Capture handler now also yields
  to the palette.
- Esc fall-through: the fallback Esc branch closed palette + slideover +
  entry modal + generator ALL on one keystroke. Now closes exactly one
  surface per keystroke, topmost first.
- Modifier chord: the real-VK Tab between username and password becomes
  Shift+Tab if the user holds Shift mid-fill -> focus moves backward and
  the password lands in the username field. ForceReleaseModifiers before
  SendVKey(VK_TAB).
- Stale placement: RestoreFromTray replayed the MinimizeToTray snapshot for
  a merely taskbar-minimised window, teleporting it to the last tray-hide
  position. Snapshot now replays only on a genuine tray return (captured
  before Show flips Visible); iconic windows use Windows' own placement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 22:46:42 +01:00
r-zakarya 3d217e82d4 fix(quick-search): Esc closes only the modal; cancel returns focus to target
- The qsInput Esc handler didn't stopPropagation, so the SAME keystroke fell
  through to the document-level Esc handlers which, seeing the modal now
  closed, also closed (or discard-prompted) the dirty slideover behind it.
- autofill/cancel now hands the foreground back to the saved target HWND:
  Esc after a fill-mode hotkey returns the user to the window they came
  from instead of leaving our app focused. Locked-vault path still ends
  focused on us (cancelAutofill runs before focusApp).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 20:20:24 +01:00
r-zakarya 3b756648c5 fix(slideover): discard-confirm on new->new switch (Ctrl+Shift+A)
Opening a fresh new entry while a dirty unsaved one was open skipped the
discard prompt: both soState.id and id are null, so soState.id !== id was
false and "switching" never triggered. Existing->anything worked (ids
differ). OR in the null/null case.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 19:52:56 +01:00
r-zakarya 2c7f6188e3 fix(clipboard): copy toasts reflect the configured clear delay
The "clears in 30s" suffix was hardcoded in ~14 toasts. Single helper
clipClearSuffix() reads clipboardClearSeconds (''=Never); prefixes concatenated
to it. Cheatsheet text made generic "(auto-clears)".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 19:42:26 +01:00
r-zakarya 8cfa1b0d6a feat(clipboard): user-configurable auto-clear delay (Never/15/30/60/120s)
Single choke point: copySecure overrides any positive clearAfterMs with the
clipboardClearSeconds setting (0 = user disabled). The 15 call sites keep
passing 30000 unchanged — positive just means "auto-clear this secret";
explicit 0 (username copies) still never clears. Synced setting + a select in
Settings > Security (Clipboard privacy). Win+V history exclusion is
deliberately NOT exposed — a password manager must not offer to leak into
history / cloud clipboard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 11:48:08 +01:00
r-zakarya 72dcc3dd82 fix(autofill): kill residual hotkey chord; dedicated fail-balloon setting
Residual new-entry trigger (1 in 6): password chars go out as
KEYEVENTF_UNICODE (VK_PACKET, can't match a hotkey) — the real chord risk is
the Ctrl+A clear-field, which sends a real VK_A. If the user re-presses
Ctrl+Shift mid-sequence, that VK_A becomes physical Ctrl+Shift+A = our own
new-entry hotkey. ForceReleaseModifiers now runs inside
SendSelectAllAndDelete, at the risky instant, not just once up front.

Balloon: was gated by "Show tray notifications" (OFF for this user) — now
gated by its own synced setting "Tray alert when autofill is blocked"
(autofillFailBalloon, Settings > Autofill, default ON), carried as notify=0
on cmd://autofill/execute. ShowBalloon no longer gates internally; each
caller applies its own setting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 05:23:02 +01:00
r-zakarya b869effee5 feat(autofill): "Clear the field before typing" setting (default ON)
The Ctrl+A + Del sent before each field misbehaves on targets where Ctrl+A
isn't select-all (terminals, some remote desktops). New synced setting
(autofillClearField, Settings > Autofill) gates it: JS appends clear=0 to
cmd://autofill/execute when off; ExecuteAutofill wraps the three
SendSelectAllAndDelete calls behind AClearFirst. Absent param = ON, so
existing behavior is unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 04:52:35 +01:00
r-zakarya a602d05b84 fix(autofill): defer the direct-hotkey success toast too
autofillFillEntry (Ctrl+Shift+L/P path) still toasted "Password filled:"
optimistically alongside the honest UIPI failure toast. Route it through
autofillPendingToast / Bridge.onAutofillResult like the quick-search path,
and label with entryDisplayName (site is often empty -> "filled:" + nothing).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 04:41:24 +01:00
r-zakarya 4a47caad55 fix(autofill): honest result reporting + restore maximized from tray
Bug 1: a maximized window trayed via the quick-search fill flow came back
"normal" on the next restore. ExecuteAutofill minimizes the window BEFORE
MinimizeToTray snapshots the placement, so the snapshot said SHOWMINIMIZED
and the never-restore-minimized guard forced SHOWNORMAL. Now honours
WPF_RESTORETOMAXIMIZED (Windows keeps the pre-minimize state in flags).

Bug 2: filling into an elevated app (admin Notepad) showed "password sent"
while UIPI silently discarded the keystrokes (SendInput even reports
success). ExecuteAutofill is now a function: it checks the target process
elevation up front (can't-open counts as elevated) and returns False without
typing. UMainForm feeds the result to JS via Bridge.onAutofillResult; the
quick-search success toast is deferred until Delphi confirms, and a failure
shows "Autofill blocked - the target window runs as administrator".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 20:19:34 +01:00
r-zakarya d583a3f7d3 refactor(js): extract attachments, autofill, unlock modules from app.js (3.1)
app.js 9363 -> 7962 lines. Three new classic-script modules:
- app.attachments.js (250): blob crypto + upload/download UI, pure
  declarations, loads before app.js
- app.autofill.js (276): Win32 combos, title->entry matching, picker,
  pure declarations, loads before app.js
- app.unlock.js (907): Quick Unlock + PIN + recovery code grouped (same
  "enter without master pw" theme); assigns Bridge.onPinResult /
  onQuickUnlockResult at top level so it loads AFTER app.js, like app.sync.js

Audit viewer stays in app.js (only 65 lines, not worth a file). Clipboard
bridge helpers stay too (were interleaved in the quick-unlock section but
unrelated). Registered in BuildAssets whitelist + index.html + APP_PARTS.
Verified in-app: quick unlock cold-start, attachment upload/download.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 19:28:19 +01:00
r-zakarya a42e4b205d feat(entries): encrypt template at rest, guided tour, import fixes, cleanup
Batched session work sharing app.js / index.html / Entries.pas, so it can't
split cleanly without interactive hunk staging.

- feat: encrypt `template` metadata at rest (template_enc/iv, added to
  ENCRYPTED_META_FIELDS). withEncryptedMeta skips an absent template key so
  partial re-ships (add-tag, move-to-folder) don't wipe it via LHasTemplate.
  Cleartext column kept as migration fallback. +3 unit tests.
- feat: first-run guided tour ("How it works") — spotlight + bubble, no GIFs,
  re-launchable from Settings, seen-flag in DPAPI prefs.
- fix(import): preserve original created_at on restore (was stamped to import
  time); restore entry icons on overwrite (PUT ignores icon_b64).
- fix(settings): correct clipboard-privacy copy (already excluded from Win+V);
  PIN text 4-6 -> 4-12; reorder Set-PIN above unlock-method; move tray/startup
  toggles to General; dedicated backup-password button + warning status; tab icons.
- chore: remove dead legacy monolith (app-legacy.js, index-legacy.html,
  style-legacy.css) + unused passkeyBtn stub.
- docs: full-source review (CODE_AUDIT 6b), template + favorite/pinned notes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 18:32:37 +01:00
r-zakarya 92ed153bc0 fix(sync): surface attachment/folder restore failures (audit 2.3)
applyRemoteSnapshot swallowed attachment + folder restore errors in silent
catch blocks. Count them (attFailed/folderFailed) and warn in a toast after
the sync summary. These don't abort the push (the entry synced, only its
attachment/folder didn't) unlike a failed entry import.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 18:32:15 +01:00
r-zakarya 45ba47f772 feat(settings): keep backup pwd on disable, dedicated pwd button, tab icons
Auto-backup no longer wipes the stored password when disabled, so
re-enabling reuses it silently. A dedicated "Set/Change backup password"
button (mirrors sync) owns the password, with a warning status when
unset. Corrected the stale hint that claimed the backup pwd was derived
from the master password. Added icons to each settings tab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 16:53:45 +01:00
r-zakarya 9bf1e4f571 fix(sync): warn when pre-sync backup has no folder (was silent no-op) 2026-07-10 12:29:07 +01:00
r-zakarya 548b26c518 fix(settings): no tab highlighted while searching (cross-tab results) 2026-07-10 12:14:33 +01:00
r-zakarya 374a6b3fe9 feat(settings): split Autofill into its own tab
Extracted the autofill toggle + hotkey combos out of the Security section into
their own .slideover-field labelled "Autofill", added an Autofill tab + map
entry. 5 tabs now.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 05:21:57 +01:00
r-zakarya c59a595d1d feat(settings): fixed-height modal + move unlock sections to Account
- Centered settings modal gets a fixed height (min(88vh,620px)) so switching
  between short/long tabs no longer resizes + re-centers it. Entry #slideover
  stays content-sized (edit/new unaffected).
- PIN unlock / Quick unlock / Recovery key moved from Sécurité to the Account
  tab (unlock methods live with the account). Autofill stays in Sécurité.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-10 05:09:11 +01:00
r-zakarya bec94930b4 feat(settings): group settings into 4 tabs
Long settings panel → 4 tabs (Général / Sécurité / Account / Sync & Backup).
Each section (.slideover-field) is keyed by its label text to a tab via
SETTINGS_TAB_OF; applySettingsTab toggles .is-tab-hidden on the rest. No HTML
restructure (sections were already .slideover-field siblings), no dep. The
existing settings search composes: a live query suspends the tab filter so
cross-tab matches show, clearing it restores the active tab.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 23:42:22 +01:00
r-zakarya f644f8cb57 fix(settings): Esc doesn't close Settings while busy overlay is up
Pressing Esc during a sync/backup closed Settings under the busy overlay.
The Esc handler already bails for open modals — added the same bail when
#busyOverlay is visible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 23:13:46 +01:00