Commit Graph

175 Commits

Author SHA1 Message Date
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 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 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 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 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 f45a17d876 fix(hotkeys): name the full combo when its key-up reaches us
The message could only cite the modifiers ("after Ctrl+Shift") because
Windows consumes the KEYDOWN of a combo it routed to another app. Its KEYUP
often still arrives though — when it does, report the exact combo the user
tried: "Ctrl+Shift+T is already used by another app".

Falls back to the modifiers-only wording when the key-up is swallowed too.
Can't misfire: an accepted combo ends the capture on keydown, so a non-modifier
key-up during capture means that key was swallowed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 18:40:08 +01:00
r-zakarya 1bcb74b0bd fix(hotkeys): detect a swallowed combo on key release, not after 10 s
"No combo captured" was both slow and wrong: the user HAD pressed a combo.
We actually receive the Ctrl/Shift/Alt keydowns — only the final key of a
combo owned by another app is swallowed by Windows. So "modifiers held, then
all released, no key ever arrived" pinpoints the situation exactly.

Reported on keyup, instantly, naming what was held: "No key registered after
Ctrl+Shift. If you did press one, another app already owns that combo — pick
a different one." The wording also holds if the user just tapped a modifier
and let go.

The 10 s timer stays as the fallback for a capture armed and abandoned with
no key at all (it would otherwise swallow every keystroke until Esc), with a
message that no longer contradicts the user.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 18:28:31 +01:00
r-zakarya c818f12c24 fix(hotkeys): release our combos while capturing a new one
A RegisterHotKey combo is delivered to the owning window, never to the
WebView. So a combo one of OUR slots holds could not be captured at all —
pressing it fired that hotkey instead (Ctrl+Shift+L ran an autofill in the
middle of the capture), and the "already used by another hotkey" collision
check was unreachable dead code. Swapping two combos was impossible.

The capture now suspends all five hotkeys (enabled=0 + vk=0 for quick-search
and new-entry, which unregisters without re-registering and is excluded from
the failure report) and restores them on finish — silently when cancelled or
timed out, with the usual toast on a successful change.

Combos held by ANOTHER app still can't be captured — that one is out of our
hands, and the 10 s timeout explains it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 18:22:36 +01:00
r-zakarya dd69747cb8 fix(hotkeys): capture times out instead of waiting forever
A combo another app already registered is consumed by Windows and delivered
to that app — our keydown listener never fires. The capture therefore sat on
"Press combo…" indefinitely with no feedback, which is exactly what a user
sees when they try to re-assign a taken combo.

Worse, while armed the capture preventDefaults every keystroke, so an
abandoned capture froze the app's keyboard until Esc.

10 s timeout that restores the button and explains the likely cause: the
combo never reached us because another app owns it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 18:15:51 +01:00
r-zakarya 3513e7b6e5 fix(ui): actionable toasts linger, and a dead hotkey shows in the sidebar
Every toast auto-dismissed after 2.8 s — fine for a success receipt, too
short to read AND act on an error. Errors and warnings now stay 10 s and can
be clicked away; success is unchanged.

The hotkey-conflict warning also needed a trace outside the toast: the ⚠ in
Settings only helps someone who opens the panel. A dot on the sidebar Settings
entry (plus a tooltip) is now the cue in the main UI that a combo is dead.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 18:10:17 +01:00
r-zakarya 4965ff2518 feat(hotkeys): persistent conflict marker in Settings
A toast is gone by the time the user opens Settings, so a hotkey Windows
refused looked perfectly configured while doing nothing. The offending row now
carries a ⚠ marker (amber border + tooltip explaining the combo is taken)
until a re-registration clears it.

Delphi reports slot ids instead of labels; HOTKEY_SLOTS maps id → button +
label in one place, so JS owns the wording and can flag the exact row. The
markers are re-applied by openSettings, surviving panel re-renders.

Also stop toasting success on the startup push (it fires on every unlock) —
autofillPushHotkeys(true). A clash still warns: that one is actionable.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 17:52:05 +01:00
r-zakarya 3d3ab3b02b fix(hotkeys): name the combo another app already owns
Two gaps when Windows refuses a RegisterHotKey:
- the toast said "one or both hotkeys" — wrong since there are three autofill
  combos, and it never said which one lost.
- Quick search and New entry failures were only written to the debug panel,
  which is off in production: the hotkey silently did nothing with no clue why.

Delphi now exposes the per-slot registration outcome and the host accumulates
the failed combo names across all five hotkeys, emitting one notification for
the batch. A combo the user deliberately cleared (vk=0) is never listed.
JS names them: "Already used by another app: 2FA code — pick another combo in
Settings".

Registration stays best-effort per slot: one clash doesn't disarm the others.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 17:46:33 +01:00
r-zakarya 7c86f35fce feat(autofill): Ctrl+Shift+T types the 2FA code
Same workflow as the password-only hotkey: click the 2FA field, press the
combo, the current 6-digit code is typed in.

Reuses the whole autofill pipeline — no new native typing path. The code is
sent in the password slot with an empty username, so Delphi's existing
password-only branch types just the code.

- Bridge: akTotp kind, hotkey id 47, registered/torn down alongside the other
  two autofill combos; an unconfigured combo (vk=0) doesn't count as a
  registration failure.
- Matching filters on totp_secret BEFORE scoring, so the multi-match picker
  never offers an entry that has no code.
- Configurable + synced like the others (autofillHotkeyTotp), collision-checked
  against the other four slots, covered by Reset to defaults and the cheatsheet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 16:53:00 +01:00
r-zakarya d40fd57133 fix(health): invalidate the dashboard cache whenever entries change
healthCache was only dropped on lock, on save and by the Recompute button.
Import, sync, delete, batch ops, duplicate, restore and the master-password
rotation all left it in place, so Vault health kept showing a pre-mutation
score and listed entries that no longer exist.

Invalidate inside loadEntries() — the choke point every mutation path already
goes through — instead of one opt-in per caller. Recompute stays lazy: it only
runs when the dashboard is next rendered.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 16:31:29 +01:00
r-zakarya 8b02a5650a fix(health): Fix button must not act when the switch was cancelled
Vault health → Fix on entry B while the editor is open and dirty on entry A:
the discard prompt appears, Cancel keeps A open — but openEntryForFix carried
on regardless. It unmasked A's password to cleartext, selected it, and pulsed
the Generate button (the blinking). Wrong entry, and it revealed a password
the user never asked to see.

openSlideOver was void, so callers couldn't tell a cancelled switch from a
successful one. It now returns false when it bails (discard cancelled, entry
not found) and true once the panel actually shows the requested entry;
openEntryForFix checks it before touching the password field.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 16:22:50 +01:00
r-zakarya 1638246a53 fix(editor): a programmatic click is not a click-outside
Import with an open, dirty editor showed "Discard unsaved changes?" stacked
on top of the native file dialog. doImport appends its hidden file input to
<body> and calls .click(); that synthetic click bubbles to the document
handler, which sees a target outside .slideover and fires
requestCloseSlideOver.

Same latent bug on the attachment download web-fallback (an <a download>
appended to <body>, then .click()ed).

Gate the handler on e.isTrusted: only a real user click can mean "clicked
away". Fixes both and any future programmatic click, at the shared choke
point. File inputs living inside the slideover (icon upload, attachment
picker) were already safe via closest('.slideover').

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 16:10:19 +01:00
r-zakarya 18fe13a861 feat(icons): preserve user-uploaded icons from bulk fetch/clear
Auto-fetched favicons and user uploads shared one icon_b64 column, so
"Clear cache" wiped custom uploads and "Re-fetch all" overwrote them — data
loss the user can't undo (a custom icon exists precisely because DDG can't
fetch that site).

New icon_custom flag (0=fetched cache, 1=user upload). Invariant: an
automatic action never touches a custom icon; only the user changes/removes
it per entry.

- Schema: icon_custom INTEGER on vault_entries.
- POST /entries/{id}/icon takes {custom}; GET /entries echoes icon_custom;
  DELETE /entries/icons/all filters COALESCE(icon_custom,0)=0.
- saveEntryIcon(id, uri, isCustom): upload→true, fetch/remove→false.
- backfillFavicons skips icon_custom even on force (Re-fetch all).
- clearAllFavicons clears only non-custom locally + refreshes the open editor
  (the icon preview didn't update on clear before).
- duplicate re-stamps a copied custom icon; import-overwrite carries the flag.

Not propagated across bulk-import / sync (the bulk INSERT doesn't read it) —
a restored icon counts as fetched. Documented; custom icons are typically
self-hosted sites DDG won't re-fetch anyway.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 15:11:25 +01:00
r-zakarya 0434eec85e refactor: remove the dead legacy entry modal
openEntryModal had zero callers — the slideover editor replaced it — but the
whole cluster still shipped: the #entryModal markup, saveEntry (wired to the
form submit), closeEntryModal, populateFolderSelect, updateEntryStrength, and
the entire tag-chip subsystem (editingTags, renderChips, addTag, the suggest
dropdown) plus their init wirings and the Esc-chain branch.

saveEntry was a latent hazard: still bound to #entryForm submit, it did a
PARTIAL PUT (no kind / totp_secret / custom_fields / template) — the exact
silent-data-loss bug moveEntryToFolder documents. Unreachable today, but one
stray call away from wiping fields.

Removed JS + HTML + the orphaned .chip-suggestion CSS. The generator is shared
with the slideover — kept; its dead 'entry' target branch collapsed to
slideover-only. 322 lines gone, tests green.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 14:35:39 +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