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>
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>
- 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>
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>
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>
The full-screen #busyOverlay (shown during sync/backup) sits outside
#settingsPanel, so a mousedown on it fired the click-outside handler and
closed Settings mid-sync. Exempted #busyOverlay like .modal/.toast already are.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clicking Sync twice started a second concurrent runSyncNow, and the click
event was passed as runSyncNow's `_attempt` retry counter (so the "Syncing…"
toast and 412-retry bound were both broken). Wrapped the handler: disable the
button while a sync runs, and call runSyncNow() with no arg. Internal retries
are unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The entry modal + register already show a strength bar; the slideover
password field didn't. Added one in soPasswordField reusing the existing
.strength-bar/--strength CSS and computeStrength (no zxcvbn). Vault health
already scores weakness via the same function.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The sync password is the only thing protecting the remote snapshot, but it
accepted 8 chars. Gate raised to 12+ chars AND computeStrength >= 50 (reused
from app.js — no zxcvbn dependency).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
User dropped the now-empty cleartext `site` + `username` columns after the
§1.3 migration completed. Removed every reference so the code matches the
schema: GET emission, POST/PUT/bulk INSERT/UPDATE (columns + params + binds).
title/tags cleartext columns still exist and are untouched.
decryptEntryMeta defaults e[f]='' for rows without *_enc (notes w/o site),
since GET no longer returns the dropped columns.
ponytail: contract phase of expand→migrate→contract; only safe because the
migration is proven complete (0 cleartext, both accounts).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
soSave re-opens the slideover on the just-saved entry to keep it visible, but
openSlideOver's edit-mode path focuses + selects the Title input — so every
Save jarringly jumped focus to the title with its text highlighted. Added an
opts.noFocus flag to openSlideOver and pass it from the post-save re-open;
normal opens (click / Enter from j/k nav) still auto-focus the title.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extends the username-at-rest scheme to site, title and tags — the last
searchable metadata still stored cleartext. Same design: dedicated
<f>_enc/<f>_iv columns (AES-GCM under the vault key), decrypted at load into
e.<f>, so client-side search/sort/render/favicon/autofill-match are unchanged.
Full-strength random-IV AES-GCM (no searchable encryption) because search is
client-side.
Generalized the helpers over ENCRYPTED_META_FIELDS = [username, site, title,
tags]:
- withEncryptedUsername → withEncryptedMeta (encrypts all four, blanks
cleartext) — wraps every POST/PUT body.
- decryptEntryUsernames → decryptEntryMeta (decrypts all four at load).
- migrateUsernamesAtRest → migrateMetadataAtRest (sweeps any field still
cleartext, live + trash).
- doChangeMasterPassword re-encrypts all four under the new key.
Server (Entries + Auth + Database):
- Columns site_enc/iv, title_enc/iv, tags_enc/iv; GET emits them (new
AddNullableField helper); POST/PUT/bulk read+persist (BindNullable helper);
rotation UPDATE re-encrypts them.
- Removed the server "Site required" validation (site='' when encrypted — the
client enforces it) at POST/PUT/bulk.
- ?q= server search neutralized (site+username ciphertext → LIKE useless; the
frontend never sends ?search=).
Tests: merge assertions updated to decrypt site (encrypted on import). 65/65.
username was runtime-validated earlier; site/title/tags NOT yet compiled/
runtime-tested (Delphi) — large multi-handler change. Rebuild BuildAssets +
PMServer, then create/edit/dup/move/tag/import/rotate and verify the DB shows
no cleartext site/title/tags (and the app still renders/searches).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
migrateUsernamesAtRest only swept state.entries (live rows), so a soft-deleted
entry kept its cleartext username in the DB until purge. Now it also fetches
+ decrypts the trash (GET /entries?deleted=1) and includes those rows in the
sweep. The PUT updates the row's fields without touching `deleted`, so the
entry stays in the trash; trashed rows aren't in the sync snapshot, so no
churn. Surfaced by a lingering cleartext username on a trashed test entry.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
username is no longer stored cleartext. New columns username_enc/username_iv
(AES-GCM under the vault key, same as encrypted_password). Search/sort/render
stay client-side, so the field is decrypted at loadEntries into e.username in
memory — everything downstream is unchanged. Full-strength random-IV AES-GCM
(no searchable/deterministic encryption) precisely because search is
client-side.
Server (PM.Handler.Entries / .Auth / PM.Database):
- Schema: vault_entries.username_enc, username_iv.
- GET returns them; POST/PUT/bulk-import read + persist them; master-pw
rotation re-encrypts them under the new key (UPDATE + loop).
- ?q= server search drops `username LIKE` (ciphertext won't match; frontend
searches client-side anyway).
Client (app.js / app.import.js):
- loadEntries/loadTrash decrypt username_enc → e.username (fallback to
cleartext for un-migrated rows).
- withEncryptedUsername(obj): write choke point — encrypts obj.username into
username_enc/username_iv and blanks the cleartext. Wraps every POST/PUT
body: saveEntry, soSave, duplicateEntry, moveEntryToFolder, addTagToEntry,
batchMove/AddTag, encryptImportEntry (import + sync-apply).
- doChangeMasterPassword re-encrypts username under the new key.
- migrateUsernamesAtRest(): one-time sweep at enterApp, PUT-re-ships rows that
still carry cleartext username so the DB gets scrubbed (bumps updated_at
once; plaintext unchanged so devices converge).
site/title/tags stay cleartext (same pattern later — see memory note). +1
merge test (username encrypted on import). 65/65.
NOT compiled/tested at runtime (Delphi) — large multi-handler change; rebuild
BuildAssets + PMServer and test create/edit/rotate/import/sync + verify the
DB shows no cleartext username.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Restoring from the tray popped a "Welcome back" toast every time — noise for
a routine action. Removed it from onTrayRestore; the auto-lock reset stays.
The login-success "Welcome back, <user>" toast is unaffected.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The profile picture (users.avatar_b64, cosmetic/unencrypted) travelled only
in the manual export. Now it's also in buildSyncSnapshot and the auto-backup
container, so a new device / a restore picks it up.
- Restore is ADDITIVE (mirrors the import path): applyRemoteSnapshot adopts
remote.avatar_b64 only when the local device has no avatar — never clobbers
a locally-set picture. No per-avatar timestamp to arbitrate, so changing an
existing avatar doesn't propagate (cosmetic, accepted).
- +2 merge tests (adopt-when-empty, don't-clobber-when-set). 65/65 green.
- Server /avatar endpoint unchanged (already accepts {avatar_b64}).
Closes the avatar item of CODE_AUDIT §4.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
deriveKeyBytes now calls NobleArgon2.argon2idAsync instead of the sync
argon2id, so it yields to the event loop periodically and the busy/unlock
spinner keeps animating instead of freezing ~0.65 s during login, register,
and master-pw rotation. Same result (both RFC-9106-verified); all callers
already await deriveKeyBytes so no call-site changes.
- Re-vendored js/argon2.js to export argon2idAsync alongside argon2id
(re-bundled from @noble/hashes@2.2.0; both variants pass the RFC 9106 §5.3
vector). 27KB → 29KB.
- Added a sync/async parity test. 63/63 green.
- Closes the last open item of CODE_AUDIT §1.2.
NOTE: argon2.js grew — run BuildAssets to re-embed it before the next
Delphi build.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Eighth slice. Quick search wasn't contiguous — its functions sat on both
sides of the cheatsheet and password-history modals (lines 725-1104). Rather
than a fiddly non-contiguous cut, the whole overlay cluster is extracted as
one byte-identical block: js/app.overlays.js (quick search + cheatsheet +
password history). Pure declarations, no top-level side effects → loads
before app.js; all state/api/Bridge/render/decryptPwd refs resolve via shared
global scope at call time.
- Byte-for-byte identical; syntax OK on all nine app parts; 62/62 tests green.
- index.html + BuildAssets whitelist + harness APP_PARTS updated.
app.js: 11936 → 9170 lines (8 modules extracted, ~2770 lines). Load order:
argon2 → crypto → totp → favicon → import → backup → health → overlays →
app → sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seventh slice of the app.js split. Moves the vault-health dashboard
(computeHealthCache, healthScoreBand, renderHealthDashboard/Section,
openEntryForFix, entryAgeDays + scoring consts) to js/app.health.js. Pure
declarations, no top-level side effects → loads before app.js. Uses
computeStrength/decryptPwd/state/api via shared global scope at call time.
- Byte-for-byte identical extraction; syntax OK on all eight app parts.
- auditCache/auditFilter sit in this var block but drive the separate
Audit-log viewer in app.js — they ride along and resolve cross-file via
shared scope (documented).
- index.html + BuildAssets whitelist + harness APP_PARTS updated.
- 62/62 tests green.
app.js: 11936 → 9545 lines (7 modules extracted).
NOTE: quick search is NOT contiguous (interleaved with cheatsheet +
history-modal code, lines 886-1063), so a clean byte-identical extraction
isn't trivial — deferred.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per user request, swap the quick-search click/key mapping so the common case
(fill just the password) is the plain left-click / Enter:
left click / Enter → password only (was: full user+Tab+pwd)
right click / Shift+Enter → full user+pwd (was: username only)
Ctrl+click / Ctrl+Enter → username only (was: password only)
Keyboard mirrors the mouse. Copy-mode (tray/palette, no HWND target) shares
the same `mode`, so it shifts too: click/Enter copies password, Ctrl+click/
Ctrl+Enter copies username (right-click's 'full' has no copy meaning → pwd).
Updated the dynamic hint, the static index.html hint, and CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sixth slice of the app.js split. Moves the favicon fetch/cache section
(faviconHost, saveEntryIcon, ensureEntryFavicon, backfillFavicons,
clearAllFavicons) to js/app.favicon.js. Pure declarations, no top-level
side effects → loads before app.js.
- Code moved byte-for-byte; no duplicate const; syntax OK on all 7 app parts.
- NEW: js/tests/favicon.test.js — 7 tests for faviconHost, the pure
URL→validated-hostname function that decides which domain is sent to the
DuckDuckGo proxy (a bug there leaks the wrong host). Covers scheme/www/
path/port stripping, non-hostname rejection, malformed dotting, unsafe
chars, and the 253-char DNS cap.
- Fixed an inaccurate source comment surfaced by the tests: it claimed raw
IPs "stay valid", but the TLD rule /\.[a-z]{2,}$/ rejects a numeric final
label, so IPs get no favicon lookup (fine). Test pins the real behaviour.
- Suite: 55 → 62 tests, all green. Assets regenerated (8 ordered JS files).
app.js: 11936 → 9790 lines (6 modules extracted).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fifth slice of the app.js split. Moves the scheduled encrypted-backup
feature (config, retention, runAutoBackupNow/runAutoBackupIfDue) to
js/app.backup.js. Pure declarations + two consts, no top-level side effects
→ loads before app.js; uses encryptExportPayload (app.import.js), Bridge,
api, state via shared global scope at call time.
- Byte-for-byte identical extraction; no duplicate const; no top-level
backup reference left in app.js; syntax OK on all six app parts.
- index.html + BuildAssets whitelist + harness APP_PARTS updated; assets
regenerated (manifest embeds all 7 ordered JS files).
- 55/55 tests green.
app.js: 11936 → 9900 lines — now under 10k. Five modules extracted
(~2000 lines): argon2 → crypto → totp → import → backup → app → sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fourth slice of the app.js split. Moves TOTP (base32Decode, generateTOTP,
parseOtpAuthUri) plus the TOTP-secret and custom-field AES-GCM wrappers to
js/app.totp.js. Loads before app.js (pure declarations), after app.crypto.js
(uses encryptPwd/decryptPwd). Also called by app.import.js and app.sync.js
via shared global scope.
- Byte-for-byte identical extraction; no duplicate const; syntax OK on all
five app parts.
- NEW: js/tests/totp.test.js — 13 tests including the 5 RFC 6238 Appendix B
reference vectors (generateTOTP reads Date.now(), so each case stubs the
sandbox clock to the vector's fixed time), base32 decode edge cases, and
parseOtpAuthUri. Extraction AND new coverage in one slice.
- Suite: 42 → 55 tests, all green.
- Assets regenerated (manifest now embeds all 6 ordered JS files:
argon2 → crypto → totp → import → app → sync); also fixes the previous
import commit's not-yet-rebuilt manifest.
- Delphi build artifacts (*.vrc, *.$manifest) gitignored.
app.js: 11936 → 10138 lines (4 modules extracted, ~1800 lines).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Third slice of the app.js split. Moves the encrypted export container,
CSV/JSON import parsing (parseCSV, findColumn, parseEntriesFromCSV,
parseEntriesFromJSON), and the doImport/doExport/doExportCSV flows to
js/app.import.js. encryptImportEntry moves here too (also called by
app.sync.js — resolved via shared global scope at call time).
- Byte-for-byte identical to the extracted block; no duplicate const;
no top-level import ref left in app.js.
- Load order: BEFORE app.js (pure declarations, no top-level side effects),
alongside app.crypto.js. Full order: argon2 → crypto → import → app → sync.
- index.html + BuildAssets whitelist + harness APP_PARTS updated.
- Safety net: the 14 CSV tests exercise parseCSV/parseEntriesFromCSV from
the extracted file and stay green (42/42).
app.js: 11936 → 10253 lines (crypto + sync + import now separate, ~1700
lines moved into 3 modules).
NOTE: assets.res not regenerated here (needs brcc32/Delphi) — run
BuildAssets before the next Delphi build to embed js/app.import.js.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second slice of the app.js split (after crypto). Moves the WebDAV sync
section to js/app.sync.js: transport (_webdavCall), buildSyncSnapshot,
applyRemoteSnapshot (merge + tombstone arbitration), runSyncNow, and the
sync settings UI.
- Byte-for-byte identical to the extracted block (verified before removal);
no duplicate const; no top-level sync reference left in app.js.
- Load order: AFTER app.js (unlike crypto, which loads before) because this
module has a top-level side effect — `Bridge.onWebdavResult = …` — that
needs Bridge/state/api already declared. Rule documented in CLAUDE.md.
- index.html + BuildAssets whitelist + harness APP_PARTS updated; assets
rebuilt to embed the new file.
- Safety net: the existing merge tests exercise applyRemoteSnapshot /
buildSyncSnapshot from the extracted file and stay green (42/42).
app.js: 11936 → 11256 lines (crypto + sync now separate).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First slice of the app.js split. Approach: ordered classic-script files
loaded via separate <script> tags (argon2.js → app.crypto.js → app.js),
NOT ES modules / a bundler. Classic scripts share one global lexical
environment, so consts/functions cross-reference across files exactly as
in the monofile — zero call-site rewrites, near-zero risk. Chosen over the
audit's esbuild/ES-module suggestion because the code is written entirely
in global scope (functions call each other by bare name everywhere).
- js/app.crypto.js: KDF (PBKDF2 + Argon2id), verifier, AES-GCM encrypt/
decrypt, key persist/restore. Verified byte-for-byte identical to the
original block before removal; no duplicate const across the two scripts.
- index.html + BuildAssets whitelist + test harness updated for the load
order. Harness CONCATENATES app.crypto.js + app.js (node:vm doesn't share
top-level const across separate runInContext calls the way browsers share
it across <script> tags); argon2.js stays a separate IIFE.
- Runtime-validated: rebuilt exe unlocks via quick-unlock and loads/decrypts
entries — the extracted crypto (restoreCryptoKey, verifierFromKeyHex,
decryptPwd) works from the separate file. 42/42 tests green.
- Docs: CLAUDE.md "Découpage frontend" (pattern + rules), file map, tests
README, CODE_AUDIT §3.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 2 of CODE_AUDIT §1.2 — live adoption of the Argon2id foundation.
Verified at runtime: a rotated account shows hash_algo=argon2id-v2 with
argon2_m=19456,t=2,p=1 in vault.db.
Server (never runs Argon2 — zero-knowledge, only stores/echoes params):
- DB: users.argon2_m/t/p columns (default 0 = PBKDF2).
- PM.Handler.Auth: HASH_ALGO_ARGON2 + param bounds, ReadArgon2Params /
AppendArgon2Params helpers. /register and /change-master-password accept
hashAlgo='argon2id-v2' + argon2:{m,t,p} and persist them; /login/challenge
echoes them. Verify path (VerifierToStoredHash/CheckVerifier) is
KDF-agnostic — the 64-hex verifier is SHA256-wrapped as for any -v2 scheme.
Client (app.js):
- state.argon2Params, cached from the challenge and persisted to
sessionStorage + the quick-unlock / PIN cold-start blobs (so a cold-started
session can still derive-from-password for reauth/rotation).
- Register + master-pw rotation derive with argon2id-v2 + ARGON2_DEFAULT_PARAMS
(OWASP m=19MiB,t=2,p=1) and send the params. Rotation re-encrypts the whole
vault under the new Argon2 key (natural migration point). Existing accounts
stay PBKDF2 until they rotate.
- Params threaded through every derive-from-password site (login, reauth,
recovery setup, change-pw current verifier). Cold-start verifier-from-raw-key
paths need no params (isDecoupledVerifierAlgo handles the -v2 wrap).
Tests: +2 param-contract tests (register<->login determinism, param
sensitivity). 42/42. Assets rebuilt to embed js/argon2.js.
Docs: CLAUDE.md auth-hash section rewritten (4 markers); CODE_AUDIT §1.2 +
table + plan marked done.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 1 of CODE_AUDIT §1.2 — additive, no live account uses Argon2id yet.
- Vendor @noble/hashes@2.2.0 argon2id as js/argon2.js (esbuild IIFE exposing
globalThis.NobleArgon2). Pure-JS, not WASM: CSP is script-src 'self' with no
wasm-unsafe-eval, so WASM would require weakening it. Verified against the
RFC 9106 §5.3 test vector. Server needs zero Argon2 (zero-knowledge: it only
ever SHA256-wraps the client verifier).
- app.js: deriveKeyBytes(pwd, salt, algo, iters, argonParams) branches Argon2id
vs PBKDF2; deriveKeyAndVerifier refactored around it. New markers
HASH_ALGO_ARGON2='argon2id-v2' + ARGON2_DEFAULT_PARAMS (OWASP m=19MiB,t=2,p=1,
~0.65s/unlock). isDecoupledVerifierAlgo() generalises the decoupled-verifier
rule to any '-v2' scheme so argon2id-v2 inherits it. AES key is still ALWAYS
the raw KDF output → entries decryptable, legacy accounts untouched.
- index.html loads js/argon2.js before app.js; added to BuildAssets whitelist;
test harness loads it into the sandbox first.
- Tests: +5 (RFC 9106 vector via vendored bundle, argon2 branch derives Argon2
key not PBKDF2, decoupled verifier, AES round-trip under Argon2 key). 40/40.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two CODE_AUDIT items in one session.
§3.2 — Frontend regression net (js/tests/, 35 tests, node:test, zero deps):
- harness.js loads app.js (monofile, no exports) into a node:vm with browser
globals stubbed, surfacing internals via an export epilogue.
- crypto: deriveKeyAndVerifier (AES key == raw PBKDF2, cross-checked vs Node
pbkdf2Sync), legacy-vs-v2 verifier decoupling, encrypt/decrypt round-trip,
IV uniqueness, AEAD tamper/wrong-key.
- csv: parseCSV tokenizer, findColumn heuristics, Bitwarden/KeePass mapping.
- merge: applyRemoteSnapshot add/update/skip (LWW), tombstone delete,
resurrection arbitration (both NaN branches), local-tombstone veto,
additive folder merge. Only api() is stubbed; loadEntries/encryptImportEntry
run for real.
- Wired as a build gate in BuildAssets.ps1 (after node --check, bypass
PM_SKIP_TESTS=1).
§2.2 — Unify timestamps on UTC:
- Entry created_at/updated_at were written via Delphi FormatDateTime(Now)
= LOCAL, while deleted_at/tombstones use SQLite CURRENT_TIMESTAMP = UTC.
The tombstone-resurrection arbitration compared the two zones, skewing by
the machine's UTC offset even single-device.
- Add NowUTC/NowUTCStr to PM.Database, swap in at every entry/attachment
write site (Entries create/update/bulk, Attachments POST echo).
- No JS change needed: arbitration now compares same-zone values.
- Existing rows self-heal on next edit (no destructive migration).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sync optimistic concurrency (ETag/If-Match)
- webdav GET captures the response ETag; PUT sends it back as If-Match so
the server rejects (412) our write when another device changed the file
between our pull and push. A 412 re-runs the whole pull→merge→push
(bounded to 3) so the other device's changes are folded in instead of
clobbered. Servers without ETags → empty etag → no If-Match → falls
back to last-write-wins (no regression). onWebdavResult gained a 4th
etag arg.
Chunked webdav PUT (big vaults no longer black-screen on sync)
- The whole encrypted snapshot base64'd into a single cmd://webdav/put URL
blew past WebView2's cap → black screen once the vault grew (20MB of
attachments). PUT bodies now stream through the file/chunk transport and
commit via a new webdav/put-commit (reads the accumulated buffer).
Chunk-transfer hang fix (root cause of the stuck "Preparing…" sync)
- All chunked transfers (saveFile/writeFile/webdav PUT) share one
reqId-keyed resolver. A resolved chunk's stale 30s timeout would later
delete the CURRENT chunk's resolver and fire the wrong res(), leaving
that chunk's await pending forever. Extracted a single _streamChunks()
helper whose ack CLEARS the pending timeout, so resolvers stay strictly
one-at-a-time. Also fixed _webdavCall referencing the Bridge-local cmd()
from module scope (latent ReferenceError).
Sync busy overlay
- syncStatus() now drives the global busy overlay too, so a running sync
blocks stray clicks (e.g. the auto-backup "Choose…" picker) and reads
like the manual backup. The account-mismatch confirm hideBusy()s first
so it's visible above the overlay.
Auto-VACUUM (reclaim space after deleting large attachments)
- SQLite never shrinks the file on DELETE, so deleting big attachments
left vault.db bloated (35MB for 11 tiny entries). DB.CompactIfBloated
VACUUMs when >20% of pages are free AND >~2MB is reclaimable — called on
startup and after each attachment delete. A healthy small vault pays
nothing. (Verified: 35MB → 695KB after the deletes.)
Rebuild: BuildAssets + F9 (UMainForm + PM.Database + PM.Handler.Attachments).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- doChangeMasterPassword shows the busy overlay while it re-encrypts the
vault: "Re-encrypting vault…" → "Re-encrypting entries… N/total" →
"Re-encrypting attachments… N/total", cleared in finally. A rotation on
a big vault took tens of seconds with no feedback before.
- Quick-unlock now SURVIVES a master-pw change instead of being wiped.
The blob stores the raw key (DPAPI-wrapped, no user secret), so it's
re-wrapped in place with the new key/salt/iters/algo (state already
holds the new values at that point). Cold-start then re-logs in with a
verifier derived from the new key. Falls back to clearing if the
re-wrap throws, so a stale old-key blob is never left behind.
- PIN blob still cleared (wrapped by PBKDF2(pin) — can't re-wrap without
the PIN). A setTimeout(0) separates the quickunlock/store and pin/clear
navigations so the back-to-back window.location.href assignments don't
coalesce and drop the re-wrap.
- Fixed a `failed` counter declaration accidentally dropped from the
attachment re-encryption loop while adding progress (ReferenceError at
runtime; node --check wouldn't catch it).
- CLAUDE.md updated for the re-wrap vs clear distinction.
Rebuild: BuildAssets + F9 (JS only this commit; F9 to re-embed).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bridge.writeFile had the same URL-length trap saveFile did: a large
auto-backup base64'd into a single cmd://file/write URL blew past
WebView2's ~2MB navigation cap, so backups of big vaults failed
silently (or blanked the page) — for BOTH the manual "Backup now"
button and the silent scheduled run.
- writeFile now streams payloads over ~1MB in chunks (reusing the same
file/chunk transport as saveFile), committed via a new
file/write-commit. Delphi shares the decode+write logic through a new
WriteDecodedFile helper and the existing FFileSaveChunks buffer.
- runAutoBackupNow(silent): the manual run shows the busy overlay
("Reading vault… N/total" → "Encrypting backup…" → "Writing file… N%")
since a 20MB backup takes ~30s; the scheduled on-unlock run passes
silent=true (no overlay, but still chunked so it no longer fails on
large vaults).
- Fixed the "Backup now" click handler passing the click Event as the
silent arg (truthy → would have suppressed the spinner and swallowed
errors); wrapped in () => runAutoBackupNow().
- Pre-sync backup benefits from the chunked writeFile automatically.
Rebuild: BuildAssets + F9 (UMainForm.pas changed).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Large file save (fixes black screen on big attachment download / export)
- Bridge.saveFile streams anything over ~1MB base64 in chunks through the
cmd:// channel instead of stuffing the whole payload in one URL — a
multi-MB base64 URL blew past WebView2's ~2MB navigation cap and blanked
the document (black screen). Small payloads keep the single-shot path.
- Chunks are sent sequentially (each acked via Bridge.onFileChunkAck
before the next) so repeated location.href assignments don't coalesce.
- Delphi accumulates chunks per reqId in a TStringBuilder
(FFileSaveChunks), commits on file/save-commit, and shares the
decode+dialog+write logic with the single-shot path via SaveDecodedFile.
- Chunk size 1MB → far fewer round-trips (a 20MB export dropped from ~67
to ~27 hops).
Busy overlay + progress
- Global spinner overlay (showBusy/updateBusy/hideBusy). doExport shows it
immediately on click — BEFORE the entry-decrypt + attachment-fetch loop
that is the real cost — with a 0ms yield so it paints before the thread
blocks (was appearing 3-5s late). Phases: "Reading vault… N/total" →
"Encrypting export…" → "Preparing file… N%" (real chunk progress).
Attachment download shows the same for files > 512KB.
- Spinner ring used an undefined --bg-elev-3 (invalid border → invisible);
switched to --border. Fixed a second stale --bg-elev-3 use on the
settings-search clear button hover.
Password reveal
- promptDialog gets an eye toggle in password mode, so every encrypted
prompt (export, import, backup password, recovery code, sync password)
can show/hide the typed value.
Avatar
- Top-right chip avatar enlarged 22px → 30px with the chip padding
rebalanced.
Rebuild: BuildAssets + F9 (UMainForm.pas changed for the chunk handlers).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The zero-knowledge verifier sent to /login used to be the raw PBKDF2
output in hex — i.e. the exact bytes of the AES key that encrypts every
entry. Intercepting a /login body (loopback, but still) handed over the
vault key. This introduces a decoupled scheme where the transmitted
verifier is a one-way function of the key.
New auth-hash scheme
- users.hash_algo 'pbkdf2-sha256-v2': the client sends
verifier = SHA256(keyHex + "pmserver/auth-verifier/v2") instead of
keyHex. Stored form is still SHA256(verifier) (identical server wrap
to 'pbkdf2-sha256'), so only the algo LABEL differs — it tells the
client which verifier formula to use. Verification needs no new server
branch (VerifierToStoredHash already SHA256-wraps any non-legacy
verifier).
- The AES key (cryptoKey) stays hex(PBKDF2) for EVERY algo, so entries
remain decryptable and switching schemes never re-encrypts data.
Adoption: new-registration + master-pw-change only
- Register and change-master-password write v2. Existing accounts keep
their algo until they rotate — the login/reauth migration signal now
fires only for LEGACY 'pbkdf2' (was: anything != CURRENT), so
sha256/v2 accounts are never force-migrated (which would have
downgraded v2 → sha256 via migrate-kdf).
Client (js/app.js): algo-aware everywhere
- verifierFromKeyHex(keyHex, algo) central helper; deriveKeyAndVerifier
/ computeVerifier take an algo arg. state.hashAlgo caches the account
scheme, set from /login/challenge, register, change-master, the
quick-unlock / PIN cold-start blobs, and the /recovery-key/redeem
response. All ~12 verifier sites updated (login, register, reauth ×4,
change-master current+new, migrate-kdf, quick-unlock + PIN cold-start,
recovery-mode current verifier).
Safety invariant: unknown/empty hashAlgo → key hex → byte-identical to
the old behaviour, so every pre-decoupling account (and every existing
quick-unlock / PIN blob without the new field) keeps working unchanged.
Verified: existing account + pre-change quick-unlock still unlocks; a
master-pw change now writes 'pbkdf2-sha256-v2' in vault.db.
Server: recovery redeem returns hashAlgo; register + change-master store
the decoupled algo; login + reauth migration signal narrowed to legacy.
Also: BuildAssets.ps1 pipes $null into node --check so the JS syntax
gate can't block on stdin in the Delphi pre-build environment.
Addresses CODE_AUDIT.md section 1.1.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Quick search (Ctrl+Shift+Q fill mode)
- Enter / left-click → full autofill (username + Tab + password), like
Ctrl+Shift+L.
- Shift+Enter / right-click → username only (new Delphi username-only
SendInput path via field=user; ExecuteAutofill AUsernameOnly param).
- Ctrl+Enter / Ctrl+click → password only.
- Copy mode (tray / palette) unchanged: Enter/left = password,
Shift+Enter/right = username.
- Clipboard fix: copy-then-minimise no longer wipes the just-copied
password — MinimizeToTray takes an AClearClipboard flag (False on the
quick-search copy path, driven by app/minimize?keepclip=1). The 30s
auto-clear still guards it.
- Right-click on a result row suppresses the native/custom context menu
(preventDefault + stopPropagation).
Editable custom-field combobox
- Option-backed custom fields (card brand, expiry year/month, etc.) now
render a custom editable combobox instead of a locked <select>: an
arrow drops a menu of ALL options (a native <datalist> filtered to the
typed text, which confused users), while the input stays freely
typeable for values not in the list. Storage shape unchanged.
- Outside-click closes the menu via the existing slideover mousedown
handler; item mousedown + preventDefault so blur doesn't race the pick.
Build safety
- BuildAssets.ps1 runs `node --check` on every embedded .js before
generating assets.res. A syntax error now aborts the asset build
(exit 1, file + line logged) instead of shipping a dead bundle that
only surfaces after a full Delphi rebuild. Node is optional: absent →
warn and continue.
Docs
- CODE_AUDIT.md: full static-analysis report (security, latent bugs,
maintainability, future features, prioritized action plan).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Profile picture / avatar
- users.avatar_b64 column (nullable, cosmetic, not encrypted) + GET/POST
/avatar endpoints mirroring the settings handler pattern.
- Top-right chip + Settings→Account show a round avatar: custom picture
if set, otherwise the username's initial on a deterministic
hash-picked colour (stable across renders).
- Upload downscales + center-crops to a 128px JPEG via FileReader →
data: URI (NOT blob:, which the CSP's `img-src 'self' data:` blocks)
before POSTing. Remove button clears it.
- Carried in the encrypted JSON export; restored on import only when the
current account has no picture (never clobbers a local one).
Tombstone restore-then-sync fix
- POST /entries and POST /entries/bulk-import now DELETE any tombstone
matching an inserted uuid (same transaction) so a restored backup
isn't re-killed on the next sync by its own stale tombstone.
- applyRemoteSnapshot arbitrates remote tombstones by timestamp: a
tombstone is skipped when the local entry with that uuid is newer than
deleted_at (resurrection wins). Ties / unparseable timestamps favour
KEEP. loadEntries() up front so updated_at reflects the live rows.
WebView2 navigation race
- Black-window-on-cold-start fix: the 1.5s nav timer no longer consumes
FPendingURL when WebView2 isn't initialised yet (it re-arms, bounded
to ~10 retries). FBrowserInitialized flag set in OnInitialized; after
the retry budget we Navigate best-effort rather than loop forever.
Sync UX
- Bidirectional toast: "pulled X new · Y updated · Z deleted · pushed N
entries" so a 0/0/0 pull still shows the vault was uploaded.
- FolderPOST/PUT: pre-declare ftString on color/icon params (fixes the
earlier [SQLite]-335 on NULL bind, already in play for CSV import).
Docs
- CLAUDE.md sync section documents tombstone purge-on-insert +
resurrection arbitration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sync engine
- Cross-account guard: refuse to merge a remote snapshot whose
username differs from the currently-signed-in one (confirm dialog,
Cancel by default) so a shared WebDAV URL / same sync password
between accounts stops silently mixing vaults.
- Local tombstones veto: skip any remote entry whose uuid is already
in the local entry_tombstones table — otherwise a perm-delete on
this device was getting undone on the next pull.
- "deleted" counter fixed: report only tombstones that actually
removed a live local entry this round, not the accumulated history
the toast used to inflate ("73 deleted" for 48 real deletes).
- Push failure surfaces syncStatus reset + toast so state doesn't
get stuck on a stale phase label.
Sync progress feedback
- Inline #syncStatus label next to the Sync button reports each
phase: Local backup… → Pulling… → Merging… → Preparing… N/total
(per-entry counter during the slow buildSyncSnapshot decrypt loop)
→ Encrypting… → Pushing…, then clears.
- Sync now / Test connection buttons are disabled while any run is
in flight so double-clicks can't kick off a concurrent sync.
Import (JSON only — CSV out of scope)
- Uuid-aware dedup: split parsed rows into fresh (new uuid) vs
overlaps (uuid already present locally).
- Overlaps prompt: confirm dialog offers Overwrite (restore/roll
back) or Skip. Overwrite PUTs the file's payload over each match;
Skip drops them and only imports fresh. Prevents the "re-import
doubles everything" regression while still allowing restore.
- Bulk-import call is skipped entirely when there's nothing fresh to
send (avoids a POST with an empty entries array).
- Template notes with empty body but populated custom_fields
(credit-card, ssh-key, etc.) no longer skipped as "note body
required" — kept as long as at least one custom field has a value.
Sidebar / uncategorised view
- "(no folder)" pseudo-entry stays visible whenever the vault has
any real folder, so it always works as a drag target for
uncategorising — and doesn't vanish mid-action when the user is
currently viewing it.
- Header title reads "(no folder)" for state.view === 'folder:All'
instead of the ambiguous "All".
- Dedicated empty-state copy for the uncategorised view.
Rebuild assets required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Slideover Save action row is now sticky at the bottom of the scrolling
body (background + top border) so it stays reachable on entries with
many custom fields or attachments — was previously buried below the
fold.
- Uncategorised view (state.view = 'folder:All'):
- Header title reads "(no folder)" instead of the ambiguous "All".
- Sidebar pseudo-entry stays visible whenever the vault has at least
one real folder, so it can be used as a drag drop-target to move
entries out of a folder even when its own count is 0.
- Empty state gets its own copy ("No uncategorised entries" + hint
to drop entries here to uncategorise) instead of the generic
"Folder is empty".
- TEST_REGRESSION.md checked in — 16-section list scoped to what the
recent Esc / drag / sync / import work touched, so post-commit
regressions can be walked through methodically instead of poking
the app ad-hoc.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Pressing Esc after editing a card silently failed: requestCloseSlideOver
opened the discard-confirm modal, then the global Esc-fallback handler
fired on the same keystroke and either closed the just-opened modal
(when Esc came on a clean slideover via the modal path) or, when
Settings was layered on top of a dirty slideover, opened the discard
prompt in the background while Settings closed.
Both panel-level Esc handlers now run in capture phase and
stopPropagation:
- Slideover handler stops only when it actually acts (slideover open,
no modal up, Settings not on top) so a single Esc opens the discard
confirm without the global fallback racing to close it.
- Settings handler stops when Settings is the active panel so the
global fallback's requestCloseSlideOver branch can't fire underneath
and pop a discard confirm on the slideover the user left dirty.
Priority order is now: open modal > Settings > slideover. First Esc on
"card dirty + Settings open" closes Settings; second Esc shows the
discard confirm. Clean slideover + Esc still closes directly.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Bitwarden CSV import: folders auto-created server-side; notes column on
login rows surfaces as a "Notes" custom field instead of polluting tags;
type=card / type=identity rows now mapped to kind=note with the
credit-card / identity template + card_* / identity_* columns
pulled into custom_fields; `fields` column parsed (Bitwarden's
"label: value\nlabel: value" lines + our own JSON shape).
- Settings panel search: live filter at top of the panel, matches each
.setting-row individually, hides whole section when no row matches,
shows a "No matches" banner. Esc clears query (without closing
Settings); Esc with empty query closes the panel.
- Quick-search hotkey customizable: SetQuickSearchHotkey added to
PM.Bridge; cmd://autofill/hotkeys extended with qs_mods/qs_vk
(independent of the autofill enabled flag — quick-search stays
armed even when autofill is off); state.quickSearchHotkey synced
via settings_json; new "Quick search picker" row in Settings.
- FireDAC SQLite folder POST/PUT: pre-declare ftString on color/icon
params so .Clear (NULL) doesn't trip "[FireDAC][Phys][SQLite]-335
type unknown" at Prepare — was crashing the CSV-import folder
auto-creation path.
- Edge form-data autocomplete suppressed on slideover inputs (title,
site, username, password, TOTP, note body, custom fields):
autocomplete=off (new-password on secrets) + spellcheck=false. Fixes
the "Informations enregistrées" dropdown popping over data after a
field was edited.
- closeSlideOver blurs any focused descendant before removing .is-open
so an invisible focused field can't react to arrow-down / backspace
after dismissal.
- Slideover Esc handler upgraded to capture phase so it fires before
the input's own keydown or browser-level Esc swallow on the active
autocomplete popup.
- Settings panel Esc closes the panel when search input is empty;
search keeps the keystroke when it has a query to clear.
- Discard-fantome on note open: customFields working copy and
originalCustomJson now share the SAME normalized array — comparing
raw plainCustom against the .map()'d working copy made notes look
dirty on open.
- Delete / Backspace global shortcut: batch-trash on normal views,
batch perm-delete on trash view, gated on selection + no input
focused + no modal up.
- Toggle thumb vertical centering via top:50% + translateY(-50%);
state checked uses translate(16px, -50%) to keep the centring.
- Batch bar disappears after per-card restore/perm-delete/trash:
state.checked.delete(id) before render for the relevant flows;
state.checked.clear() before render in emptyTrash and the new
moveEntriesToFolder helper.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Sync (WebDAV, auto-merge): UUID + tombstones foundations (server +
JS), THTTPClient bridge cmds (get/put/test), runSyncNow engine with
pull/merge/push flow, Settings UI, pre-sync backup option. Test
connection now treats 404 as OK (snapshot not yet created) and 401/
403 as auth failure with dedicated toast.
- Batch drag-drop: cards + table rows carry checked-set ids (CSV) when
dragged from an active selection; folder + trash drop handlers parse
and apply in batch via new moveEntriesToFolder helper that preserves
TOTP / custom_fields / kind in the full PUT payload.
- Clean shutdown: WM_QUERYENDSESSION / WM_ENDSESSION captured in the
bridge message-only window; FormCloseQuery bypasses the tray-minimize
intercept on system shutdown / restart / logoff so FireDAC closes the
SQLite WAL cleanly instead of leaving -shm / -wal residue after a
force-kill.
- Center-mode modal: blur+dim backdrop via body::before pseudo-element
in editor-position=center, swallows clicks below the panel so the
existing outside-click handlers reliably dismiss the slideover /
settings panel.
- Batch bar state fixes: state.checked cleared before render in
moveEntriesToFolder, emptyTrash, and per-card restoreEntry /
permanentDelete / deleteEntry so the action bar disappears once the
selection is fully processed.
- Save-then-discard duplicate fix: soState reset to null before
openSlideOver re-opens the freshly saved entry, otherwise the dirty
check fired on the soState.id=null → newId switch and a Cancel left
the form in new-entry mode (second Save → POST duplicate).
- TEST_SYNC.md: end-to-end checklist for validating the WebDAV sync
with 2 real instances.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- PIN unlock: device-local 4-12 digit shortcut, DPAPI-wrapped vault
key. Three modes (state.unlockMode): pw / pin / pw+pin. PIN
derives a wrap key via PBKDF2(pin, salt, 100k) and unwraps the
stored vault key (mirrors the Quick Unlock blob shape).
Anti-brute-force: 5 wrong attempts wipes the blob. Setup gated by
master-pw reauth so an unattended unlocked laptop can't be
backdoored. Master pw rotation clears the PIN blob (key drift).
loadServerSettings post-sync demotes pin/both -> pw when the local
blob is missing, so a wiped device re-syncs the correct mode up.
New unit PM.PinUnlock.pas + cmd://pin/{store,get,clear,status}.
- Table column picker: ⚙ in topbar (table view only), checkbox menu
for Site/Username/Folder/Updated. Site also drives showSiteOnCards
so the existing "Show site / URL" toggle in Settings stays in
sync. NAME column auto-widths (180px min, content max, +32px
right padding) so column hugs the next one without truncating.
- Editor position chooser (Appearance setting): Slide-over right /
left / Centered modal. Scoped to #slideover + #settingsPanel so
the click-outside / pointer-events logic doesn't accidentally
trap the modal-style empty viewport.
- Confirm before discarding unsaved edits: state.confirmOnUnsaved
setting (default ON), prompts on X / Esc / click-outside / switch-
to-other-entry. Also gates Lock vault / Sign out actions when the
editor is dirty; auto-lock and system-lock paths bypass to avoid
blocking on an unattended machine.
- Open-in-browser button added to the actions cell of the table
view (was card-only).
- Entry templates pass folder customization + template id through
duplicate / export / import / auto-backup roundtrips.
- Folder color + icon now persisted across export/import: payload.
folders carries name/color/icon; import creates missing folders
additively (existing local customisation kept).
- Bulk move-to-folder, batch add-tag, single add-tag now re-ship
the full entry payload so partial PUTs don't silently wipe
TOTP / custom_fields / kind / template.
- FireDAC: switched ftString -> ftMemo for icon_b64 / custom_fields
/ TOTP / template params and replaced .AsString with .Value so a
large (~200 KB) DeepSeek favicon no longer gets truncated at the
default ANSI 4000-char cap.
- Unicode filenames: attachment INSERT now uses ftWideString +
.AsWideString so non-ANSI filenames round-trip instead of being
mangled to "?".
- HandleSetEntryIcon cap raised 256 KB -> 512 KB chars to accept
base64 data URIs produced by max-raw favicon fetches.
- promptDialog + askReauth support inline `error` line + retry-
with-count loops on doExport reauth and auto-backup password
setup (5 attempts cap before bailing).
- Recently used moved from Tools to Vault section in the sidebar.
- Auth screen passkey button hidden (Delphi backend stubs WebAuthn).
- Sensitive cmd://favicon/refresh-style buttons in Settings now
stopPropagation so the document-level "close panel" handler
doesn't dismiss Settings mid-async during DOM reparenting.
- TEST_PLAN.md: +PIN unlock section.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Entry templates: new vault_entries.template column drives a typed
sub-kind ('credit-card', 'ssh-key', 'server', 'recovery-codes'). Card
+ table label off the template, badge reads "credit card" instead of
"note". Templates seed kind=note (no site/password required), use
custom_fields with optional dropdown options (brand, month/year,
protocol). Round-tripped across export/import/duplicate/master-pw
rotation, preserved by partial PUTs via a HasTemplate flag.
- Custom fields: support per-field `options[]` rendering as <select>
(card brand, expiry MM/YYYY, SSH/server protocol).
- Tags: existing-tag autocomplete dropdown under the chip input,
filtered against what's already selected.
- Search history: per-query X for individual delete + 1s debounced
commit (no Enter required).
- Slideover: clicking outside closes again (drag-selection respected
via mousedown origin tracker), Esc closes, X closes. App shell is
pushed left by 420px when the panel is open so the table / pagination
/ sort / search stay visible and interactive.
- Export/import: JSON now round-trips custom_fields, attachments
(decrypted to base64, re-encrypted under current key on restore),
icon_b64, and template. CSV warning lists what's not included.
- Auto-backup: same payload shape as user-driven export.
- Notes: import (JSON + CSV) accepts kind=note with empty site,
preserves title/template/custom_fields. CSV parser detects kind/
template columns.
- Bulk-import response returns `ids[]` parallel to input so the
client can map back to new entry IDs (drives attachment restore).
- Move-to-folder bugs fixed: moveEntryToFolder, batchMoveToFolder,
addTag, batchAddTag were all silently wiping TOTP / custom_fields
/ kind / template via partial PUT. Now re-ship full payload.
- Master-pw rotation: server mints a fresh session token + csrf so
the very next request after rotation no longer ESessionRejects.
Client adopts the new pair. Attachments are re-encrypted client-side
during rotation (GET old → decrypt with old key → encrypt with new
→ PUT). New endpoints: GET /attachments/all, PUT /attachments/:id.
- Duplicate: carries icon_b64 + template + attachments to the copy.
- HandleCreateEntry: accepts icon_b64.
- FireDAC param fix: all blob/icon/custom_fields params use ftMemo +
.Value assignment so SQLite TEXT no longer truncates to 4000 chars
(deepseek's 200+ KB favicon was being wiped on lock/unlock).
- HandleSetEntryIcon cap: 262144 → 524288 chars (base64 of a 256 KB
raw fetch overflows the old cap, fails silently in saveEntryIcon).
- Native save dialog: surfaces server errors instead of swallowing.
- Modals: reauth (export) + backup-password prompt support inline
error display, retry up to 5 attempts, then hard-stop.
- Keyboard cursor (j/k): bootstraps to current page, auto-paginates
when the cursor crosses a page boundary, Enter opens slideover.
- Slideover focuses Title on edit-open so j/k → Enter → type Just
Works.
- TOTP tool: Esc closes the modal.
- App version + launch mode (auto/manual): exposed via bridge,
surfaced in Settings → Account. Autostart launches suppress the
first-time tray balloon.
- Passkey button hidden (Delphi backend stubs WebAuthn at 501).
- TEST_PLAN.md captured for regression coverage.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Big feature trio
- Secure notes (kind='login'|'note') reusing the encrypted_password+iv
pipeline for the body. New sidebar entry, slideover variant (title +
multiline body), distinct card / table-view rendering, badge in name
column, copy-content button replacing the password copy on note rows.
- Password history: entries_password_history table keeps up to 20 prior
ciphertexts per entry. HandleUpdateEntry pushes the pre-update
encrypted_password into history ONLY when it actually differs from
the incoming one (JS reuses originalEncrypted bit-for-bit when the
plaintext is unchanged — avoids spamming history on title/folder edits).
GET /entries/{id}/history endpoint. Slideover modal lists versions
with mask/reveal/copy/revert. Master-pw rotation wipes history (old
ciphertext can't be decrypted with the new key).
- Custom fields: per-entry encrypted JSON array of {label, value,
is_secret}. Same crypto pipeline as the password. Slideover row UI
with label/value inputs, secret toggle (eye), copy, delete. Re-
encryption flows through bulk-import, change-master-password, and
duplicate.
Quick wins
- Cheatsheet overlay (press '?' or topbar button or Ctrl+K). Lists all
hotkeys + global / tray / card actions. SVG icons inline so the
cheatsheet matches the actual app glyphs (no emoji mismatch).
- Open URL button on entry cards: ShellExecute via cmd://app/open-url,
http(s) only, validates entry.site looks like a real hostname.
- Trash auto-purge: setting "Empty trash after N days" (never/7/30/90).
DELETE /entries/trash/old?days=N called at every unlock.
Favicon strategy
- Subdomains (chat.deepseek.com, app.X.com…) now try the SLD first
(deepseek.com.ico) before the full host. DDG often returns a generic
placeholder for subdomains that passes the byte threshold; the SLD-first
switch surfaces the real brand icon.
- Cap bumped 64 KB → 256 KB on all three sides (Delphi fetch, server
endpoint, JS upload). DDG sometimes serves the full-res asset.
UX polish
- Click-outside-slideover: stopPropagation everywhere it bites. Custom
fields buttons (add / delete / secret toggle / copy / eye) all stop
the click bubble so the document-level "close on outside click" handler
doesn't fire when rerender() detaches the target from the DOM.
- Native search-cancel button restyled: cyan accent X via mask-image,
cursor: pointer, breathing room before the Ctrl+K kbd chip.
- Password history modal: scrollable body, multiline wrapped passwords,
hover border highlight.
- Cheatsheet panel widened (560 → 720 px) so the descriptions no longer
ellipsis-clip.
- "+ New" topbar splits into a small dropdown: New login / New note.
- Notes show a "note" badge in table-view name column, italic
"Encrypted note" placeholder in the username column.
Internals
- duplicateEntry copies kind + custom_fields too (one-line forgotten
earlier).
- entries_password_history dropped on master-pw rotation — the old
ciphertexts are unrecoverable with the new key.
- bulk-import re-encryption path includes custom_fields.
CLAUDE.md
- "Entry payload — call sites à toucher ensemble" lists the 6 spots
to update when adding a new (en)crypted field. Notes the historical
miss of kind in duplicateEntry and custom_fields in the rotation +
duplicate.
Repo hygiene
- .gitattributes forces CRLF on Delphi sources (RAD Studio refuses LF).
text=auto for web frontend / docs, binary for .res / .exe / images.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Ctrl+Shift+Q quick-search + autofill
- New global hotkey: capture the foreground HWND, restore the window
if hidden, pop the quick-search modal in "fill mode". On pick, the
password is SendInput'd into the saved HWND — no clipboard touch.
- hide_after flag added to cmd://autofill/execute: when set (tray-mode
hotkey), Delphi MinimizeToTray's *after* SendInput completes. Hiding
before SendInput would trip Win10/11 anti-focus-stealing rules and
block focus handoff to the target.
- Quick-search modal hint text adapts to fill vs copy mode.
- Esc / close in fill mode sends cmd://autofill/cancel so a stale
HWND doesn't get reused by an unrelated Ctrl+Shift+L later.
Compile-time browser engine switch
- {.$DEFINE USE_EDGE_BROWSER} in UMainForm.pas selects between
TTMSFNCWebBrowser (default, cross-platform abstraction) and
TTMSFNCEdgeWebBrowser (Windows-only WebView2 wrapper). Both
inherit from TTMSFNCCustomWebBrowser so the bridge cmd:// glue is
unchanged; the field type is a conditional alias TWebBrowserClass.
- WebBrowser is created dynamically in FormCreate so neither variant
needs a second .fmx. Events are wired BEFORE Parent assignment so
OnInitialized doesn't race the WebView2 async init on fast/pre-warmed
Edge installs (was silently missing the disable-context-menu /
disable-accelerator-keys calls).
- Native context menu disabled by assigning an empty PopupMenu1 (works
for both backends, unlike OnGetContextMenu which is publish-gated
via {$IFNDEF FNCLIB} on TTMSFNCWebBrowser).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Quick search from tray
- New "Quick search…" entry in the tray context menu (between Open
and Lock vault).
- Compact modal with live-filtered top-8 entries, arrow keys / Enter
to copy the password (Shift+Enter copies the username instead),
Esc to dismiss. Each row shows the favicon when cached.
- Locked vault → focus the master password input instead of opening
the modal (same pattern as the locked-autofill-hotkey path).
- Window-state restore: Delphi remembers whether the window was
hidden before the menu was opened and tells JS via the
Bridge.openQuickSearch(wasHidden) arg. After the copy (or cancel)
we hide back to the tray so the previously-foreground app comes
back and Ctrl+V drops the password in.
Tray notifications toggle
- New Settings → Security "Show tray notifications" toggle. Gates
Shell_NotifyIcon NIF_INFO balloons (currently only the "still
running in the tray" first-time popup). Default ON, synced via
settings_json so it follows the user across devices.
- PM.Bridge.ShowNotifications exposed as a public property; JS
pushes the value on every settings sync.
Privacy: WebView2 phone-home killed
- WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS set in the unit
initialization section (before the TMS WebBrowser instantiates
its CoreWebView2Environment). Disables: background networking,
sync, component updates, breakpad/crashpad, domain reliability,
client-side phishing detection, experiments, UMA upload,
MediaRouter, OptimizationHints, SafeBrowsing enhanced, autofill
server, privacy sandbox APIs. Verified via Resource Monitor: only
127.0.0.1 connections remain (plus DDG when favicons are on).
Fixes
- Blank-window-on-launch race: the 1.5 s navigation timer assumes
WebView2 finishes init in time, but on slow machines Edge
Chromium needs 2-3 s and the Navigate() call is silently
dropped. WebBrowserInitialized now also navigates if a URL is
still pending — first to run wins.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Unified create/edit slideover
- openSlideOver(id) now accepts null for new entries. Same UI
(icon, name, site, user, password, TOTP, folder, tags) for both
create and edit. Drops the separate entry modal — no more "save
first, then add TOTP" two-step.
- "+ New" button, Ctrl+K → New entry, and Ctrl+Shift+A all route
through the slideover. Ctrl+Shift+A pre-fills the title with the
foreground window's name.
- Save button visible from the start in new mode (no dirty wait).
- Title shows mode unambiguously: cyan "+ New entry" vs
"Edit · <name>".
Custom icon upload (soIconField)
- 56×56 preview at the top of every slideover + Upload icon /
Remove buttons. Same POST /entries/{id}/icon endpoint as the
auto-fetch path. Validates type / size (64 KB cap matching server).
- Solves the case where DDG doesn't index a domain (self-hosted
apps, private sites): the user pastes any image and it sticks.
Favicon: privacy-first, DDG only
- Removed the direct-fetch fallback steps (3-5). Privacy stance:
zero DNS leak outside icons.duckduckgo.com. Domains DDG doesn't
cover stay icon-less until the user uploads a custom one.
- PM.Favicon.FetchFaviconDataUri takes an optional TFaviconLog
callback so UMainForm can stream per-step trace into LogLine for
diagnostics.
Fixes
- Slideover z-index 30 → 50. The topbar's backdrop-filter creates a
stacking context at z-index 40 which was clipping the slideover
header (title + close button hidden behind topbar).
- RestoreFromTray no longer un-maximises a maximised window when
called outside a tray-restore context (Ctrl+Shift+A, Ctrl+Shift+L
picker, app/focus cmd). SW_RESTORE on a maximised window reverts
to normal — now we only SW_RESTORE if IsIconic.
- "Show all"/"Show less" per-category state survives renderGrid
re-renders (healthExpanded map).
- "+ New" and dashboard "Fix" buttons stopPropagation so the
document-level click-outside handler doesn't close the slideover
they just opened.
- soDirtyCheck keeps Save visible while in new mode regardless of
diff.
- openSlideover → openSlideOver typo fix across all call sites.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>