Commit Graph

62 Commits

Author SHA1 Message Date
r-zakarya 0604cca148 feat(autofill): un bouton Open, et le menu ne passe plus sous la barrette
Quand la barrette est allumee, elle est souvent la seule chose de
l'application a l'ecran : revenir au coffre ne doit pas demander de
retrouver l'icone du tray.

Le menu d'opacite passait derriere : le battement qui repose la barrette
en tete de la bande TOPMOST cinq fois par seconde recouvrait le menu que
la barrette venait d'ouvrir - lui aussi dans cette bande. TrackPopupMenu
est modal, donc le minuteur battait pendant tout ce temps. Il s'arrete
maintenant le temps du menu.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 17:35:36 +01:00
r-zakarya 6b908e73a5 fix(autofill): la barrette reste au-dessus, et se traine depuis ses boutons
TOPMOST n'est pas un rang, c'est une bande. La barre des taches y est
aussi, et cliquer son fond vide la remonte en tete de cette bande : une
barrette posee dessus passe DERRIERE et semble avoir disparu. Le meme
battement de 200 ms la repose.

Traine depuis un bouton : les boutons sont subclasses, et DragDetect
tranche entre clic et glissement en respectant le seuil regle par
l'utilisateur. Sur un clic, on envoie BN_CLICKED a la place du bouton -
DragDetect a avale le WM_LBUTTONUP, lui rendre la main le laisserait
enfonce pour toujours.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 17:27:38 +01:00
r-zakarya 9ce07ae5d0 feat(autofill): la barrette revient la ou on l'avait posee
Pref DPAPI lazyPos, ecrite sur WM_EXITSIZEMOVE - une ecriture par
deplacement, pas une par pixel parcouru.

La position n'est reprise que si MonitorFromRect la trouve encore sur un
ecran. La barrette n'a pas d'entree dans la barre des taches : une
position heritee d'un ecran debranche, ou d'une resolution changee, la
laisserait hors du bureau et definitivement irrecuperable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 16:20:02 +01:00
r-zakarya 7fa26858e3 feat(autofill): la barrette s'efface quand la souris la quitte
Clic droit dessus -> 100/75/50/25 %, persiste en pref DPAPI. Pas dans
settings_json : la barrette peut etre allumee coffre ferme, ou aucune
session n'existe pour lire les reglages du serveur.

Le survol est sonde toutes les 200 ms plutot que suivi par
TrackMouseEvent : avec quatre fenetres enfants, passer de la barrette a
un bouton envoie deja un WM_MOUSELEAVE au parent, et il faudrait suivre
la souris sur les cinq fenetres pour recoller les morceaux.

Le menu appartient a la fenetre message-only : TrackPopupMenu reclame un
SetForegroundWindow pour se refermer seul, et le faire sur la barrette
detruirait le NOACTIVATE qui la fait fonctionner.

Transparente n'est pas traversante - les clics sont toujours avales.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 16:14:15 +01:00
r-zakarya f7665b4225 feat(autofill): fermer la barrette depuis la barrette, et des libelles lisibles
Un quatrieme bouton X. Il POSTE WM_LAZY_CLOSE a la fenetre message-only
plutot que de detruire sur place : liberer une fenetre depuis son propre
gestionnaire libererait, en plein envoi, l'instance qui lui sert de
window proc.

Au passage, deux libelles sortaient en mojibake - "Quick search__|" dans
le menu du tray, un tiret cadratin dans l'infobulle. Le fichier est en
UTF-8 SANS BOM, que Delphi lit en ANSI, un octet par caractere : les
litteraux restent donc en ASCII.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 15:52:50 +01:00
r-zakarya 52f5024de0 feat(autofill): un troisieme bouton 2FA, et la barrette suit le theme
SetPreferredAppMode(ForceDark) ne themise que ce que le systeme dessine
lui-meme - menus, ascenseurs, infobulles. Un bouton Win32 standard reste
donc gris clair sur fond sombre, ce que la capture montrait.

BS_OWNERDRAW + WM_DRAWITEM : le systeme garde le suivi de la souris et
l'etat enfonce, nous ne reprenons que les couleurs, recopiees des
variables de css/style.css. Le cadre passe dans WM_ERASEBKGND plutot que
par WS_BORDER, qui se sert dans les couleurs systeme.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 15:49:49 +01:00
r-zakarya 09bbd3fe9e feat(autofill): lazy mode - une barrette de deux boutons pour remplir a la souris
Menu du tray -> "Lazy mode". Deux boutons Full / Password qui flottent
au-dessus de tout et font ce que font Ctrl+Shift+L / Ctrl+Shift+P.

WS_EX_NOACTIVATE est toute l'astuce : une fenetre ordinaire prendrait le
premier plan au clic, le champ que l'utilisateur vient de cliquer perdrait
son curseur, et GetForegroundWindow nous designerait nous-memes comme cible.
Le clic ne deplace donc jamais le premier plan.

Le corps du handler WM_HOTKEY est extrait dans FireAutofillRequest : hotkey
et barrette partagent la capture de la cible, le matching de titre et le
picker. Un seul chemin, deux gestes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-26 15:42:49 +01:00
r-zakarya d6701a8994 fix(hotkeys): attach to the foreground thread to focus the buried dialog
The autofill helper attaches the caller to the TARGET thread, but the dialog runs on our own thread, so the attach was a no-op and Windows kept refusing the foreground change - the dialog rose without focus. Attaching to the current foreground thread instead is what grants the call.
2026-08-01 18:53:20 +01:00
r-zakarya c9c1e98470 fix(hotkeys): focus the buried dialog and say why the shortcut did nothing
SetForegroundWindow is refused when the hotkey comes from another application, so the dialog was raised but never focused; ForceForegroundWindow is now exposed by PM.Bridge for that. The explanation goes through a tray balloon because our window is disabled and behind the dialog, where no toast would be seen.
2026-08-01 18:49:03 +01:00
r-zakarya 38ba208455 fix(autofill): stop sending Ctrl+A after the Tab in a full fill
Tabbing into an input already selects its whole content, so the second clear was redundant. Where Tab lands on something that is not editable - a link, a reveal button, the document, as on the Reddit login form - Ctrl+A selected the entire page instead and the password went nowhere.

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 23:10:22 +01:00
r-zakarya 0506d757dd fix(startup): hide the taskbar proxy too, and trace the --tray timeline
The window still showed, taskbar entry included. Two things.

The taskbar button belongs to the FMX proxy window (TFMAppClass), a separate
top-level window from the form — hiding the form alone never removed it.
FindFMXAppWindow moves from private to public so the activate-hide can drop
both. (Guarded on Assigned(FBridge): the handler is installed at line 222,
the bridge is created at 268.)

And rather than guess a fourth time at FMX's startup order — the same trap as
the window-state saga, which only fell to instrumentation — this logs a
millisecond timeline: FormCreate begin/end, the activate-hide, and the
deferred MinimizeToTray. Whichever gap holds the second will be obvious.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:47:25 +01:00
r-zakarya 6bed75f49e fix(window): a maximised window comes back maximised after Show desktop
Win+D / Show desktop minimises the window without any tray transition, so the
restore took the non-tray branch. FMainForm.Show runs first there and
re-applies FMX's own WindowState (normal), discarding the
WPF_RESTORETOMAXIMIZED flag Windows had recorded — the app returned in normal
size.

Read the placement flag BEFORE showing, and re-apply SW_SHOWMAXIMIZED when it
was set. The tray path is untouched: it already replays its own captured
placement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 19:50:24 +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 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 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 a8e0a3da00 fix(autofill): unregister new-entry hotkey during SendInput (chord fire)
Ctrl+Shift+P (password autofill) with Ctrl+Shift physically held triggered
new-entry: the clear-field step injects a real Ctrl+A, and the held modifiers
(re-pressed by keyboard auto-repeat faster than ForceReleaseModifiers clears
them) make the system see Ctrl+Shift+A = our new-entry global hotkey. Injected
keys are only Ctrl+A + Tab, and only 'A' matches a hotkey, so unregister
NEW_ENTRY around the SendInput (try/finally re-register) — no WM_HOTKEY is
generated. Password/username go as VK_PACKET unicode, can't chord.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 19:24:09 +01:00
r-zakarya 2e7aebf651 fix(db): enable SQLite foreign_keys explicitly (audit 6c-9)
SQLite defaults foreign_keys OFF per connection, and the schema relies on
ON DELETE CASCADE (entry_attachments, entries_password_history, recovery_keys).
FireDAC is said to default it On but that's undocumented here — set
ForeignKeys=On so a permanent delete never orphans encrypted blobs. Correct
in both cases (no-op if already On). Base check before fix: 0 orphans.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 05:33:46 +01:00
r-zakarya 19b66b7bbd fix(csrf): validate against the presented session, not the most recent (6c-3)
RequireCSRF looked up csrf_token by user_id ORDER BY created_at DESC LIMIT 1
— the newest session, not the one whose Bearer token was presented. Two
consequences: created_at's 1-second resolution made two same-second sessions
a coin-flip (phantom 403s), and conceptually it accepted "any valid CSRF for
this user" rather than the one bound to this session. Now queried by
token_hash = SHA256(bearer) AND user_id. Authenticate already located this
row + checked expiry, so the expiry clause is dropped. Signature unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 05:30:16 +01:00
r-zakarya c2cc9c72e6 fix(session): write expires_at in UTC, not local time (audit 6c-2)
CreateSession wrote FormatDateTime(IncHour(Now,24)) = local, but RequireCSRF
and CleanupExpired compare against SQLite datetime('now') = UTC. On negative
UTC offsets the CSRF window fell short of the 24h auth window: between the two,
GETs authed fine but every write got 403 "Invalid CSRF token". Now
IncHour(NowUTC,24) on write + LExpires < NowUTC in Authenticate, so all three
readers are UTC-consistent. Last FormatDateTime(...,Now) in the backend —
closes §2.2 for real. NowUTC + DateUtils already in uses.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 05:25:37 +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 3822c21194 fix(clipboard): auto-clear must not wipe content copied since our SetText
Copy a password (30s auto-clear armed), then copy something else from
another app before the timer fires: the tick emptied the clipboard anyway,
destroying the user's newer content. Guard with the Win32 clipboard sequence
number: SetText snapshots GetClipboardSequenceNumber, ClearIfOurs only
empties when it hasn't moved. Applied to the auto-clear timer AND the
clear-on-minimize path (same bug class); the explicit JS clipboard/clear
command stays unconditional (user-initiated).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 11:20:49 +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 9a72fc0424 fix(build): ShowBalloon must be public (called from UMainForm)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 05:12:23 +01:00
r-zakarya aab0b14be4 fix(autofill): force-release stuck modifiers; balloon when blocked from tray
Root cause of "Ctrl+Shift+P opened the new-entry modal": if the user still
holds Ctrl+Shift when WaitForModifierRelease times out (1s), every password
letter is typed as a Ctrl+Shift+<letter> chord — garbage in the field AND it
fires our own global hotkeys (a password containing 'a' triggers Ctrl+Shift+A
= new entry). ForceReleaseModifiers now injects KEYUP for any still-held
modifier before typing.

Also: when the fill is blocked (elevated target) while the window is hidden
in the tray, the in-app toast is invisible — show a tray balloon instead.
ShowFirstTimeBalloon generalized into ShowBalloon(title, text, warning),
gated by the existing "Show tray notifications" setting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 05:08:50 +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 da1284cfc3 fix(build): declare PROCESS_QUERY_LIMITED_INFORMATION locally
Winapi.Windows doesn't expose the WinNT.h constant — dcc32 E2003.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 04:41:38 +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 7103fbf703 fix(quick-search): keep the open window visible during the fill (no minimize)
The minimize-then-SW_SHOWNOACTIVATE approach flickered and sometimes lost the
restore race (window stayed minimized). Root fix: when the window was open
before the hotkey (ARestoreAfter), skip the minimize entirely — being the
foreground process is exactly what allows handing focus to the target, so the
window simply stays in place beside it. Tray-origin flow keeps the old
minimize (the window is a temporary overlay, trayed after the fill anyway).

Safety: if the target refuses the foreground (elevated / UIPI) and our window
is still foreground, bail before typing — otherwise the password would be
typed into the vault's own visible UI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 20:01:05 +01:00
r-zakarya 3b32a621e8 fix(quick-search): stop teleporting + hiding an already-open window
Two bugs when Ctrl+Shift+Q fires while the app window is open beside the
target app:
- RestoreFromTray re-applied FSavedPlacement (captured at the LAST
  MinimizeToTray) to an already-visible window -> it jumped to a stale
  position. Now: visible and not iconic -> just SetForegroundWindow.
- ExecuteAutofill minimizes our window when it is foreground (the user just
  clicked the entry) and never brought it back when hide_after was false.
  New ARestoreAfter param (= not HideAfter): restore with SW_SHOWNOACTIVATE
  after the fill, so the window returns to its position without stealing
  focus from the freshly-filled target.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 19:52:26 +01:00
r-zakarya 01426a48b1 refactor(db): drop cleartext template refs, fix ghost columns + fresh-DB schema
Migration verified (0 unmigrated cleartext templates across both accounts)
before contracting:
- GET/POST/PUT/bulk no longer read or write the cleartext template column
  (only template_enc/iv); the PUT partial-update gate stays keyed on the
  'template' JSON key presence.
- Removed AddColumnIfMissing for template AND tags/title — those two had
  silently re-added the dropped columns as empty ghosts at every start.
- CREATE TABLE: removed site/username NOT NULL cleartext columns — a FRESH
  database rejected the very first INSERT (which no longer ships them).

User can now DROP COLUMN template (and re-drop the ghost tags/title).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 19:52:26 +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 b023eab1f4 fix(http): return 401 (not 500) on expired/rejected session
Authenticate/RequireCSRF write a 401 then raise ESessionRejected; when it
reached the dispatcher catch-all, the generic `on E: Exception` overwrote it
with a 500. Added `on ESessionRejected do Exit` before the generic clause in
both dispatchers (GET + Other) — one place, covers every handler whether or
not it wraps Authenticate. Root cause, not per-handler patch.

ponytail: runtime check only (expired token → 401) — no Delphi unit harness.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 21:42:08 +01:00
r-zakarya 6556ce8dea feat(crypto): encrypt site/title/tags at rest too (CODE_AUDIT §1.3)
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>
2026-07-09 11:25:13 +01:00
r-zakarya 69fb2b10dd feat(crypto): encrypt username at rest (CODE_AUDIT §1.3)
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>
2026-07-08 22:04:48 +01:00
r-zakarya 5e88ad33d1 feat(crypto): adopt Argon2id (argon2id-v2) on register + master-pw change
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>
2026-07-05 14:52:11 +01:00
r-zakarya d9397881dc test: add frontend unit suite + fix mixed local/UTC timestamps
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>
2026-07-04 19:17:33 +01:00
r-zakarya ac909f4f09 feat: sync ETag concurrency + fix chunk-transfer hang + sync overlay + auto-VACUUM
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>
2026-07-04 00:02:25 +01:00
r-zakarya 3076fec710 feat: quick-search fill modes + editable custom-field combobox + JS build gate
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>
2026-07-03 08:13:25 +01:00
r-zakarya 7440d07793 feat: profile avatar + tombstone-restore fix + WebView2 nav race + sync summary
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>
2026-07-02 23:36:36 +01:00
r-zakarya bd14831449 feat: Bitwarden import bundle + settings search + quick-search hotkey + UX fixes
- 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>
2026-06-30 23:25:50 +01:00
r-zakarya 6869b7c692 feat: WebDAV sync + batch DnD + clean shutdown + center-modal UX bundle
- 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>
2026-06-30 00:32:12 +01:00
r-zakarya b00da43ab0 feat: PIN unlock + table column picker + edit-position chooser + UX
- 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>
2026-06-29 04:41:39 +01:00
r-zakarya e23a78dda7 feat: entry templates + tag autocomplete + slideover push + robustness bundle
- 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>
2026-06-26 21:20:07 +01:00
Zaki fa7ea191be feat: native save + auto-backup + folder customization + attachments + UX bundle
- File: native Save As dialog via Bridge.saveFile (replaces WebView2
  browser download popup) for encrypted JSON + CSV exports.
- Auto-backup: silent periodic encrypted JSON to a chosen folder,
  user-set interval + retention, separate DPAPI-stored password, runs
  5s after unlock if due. New file/* bridge cmds (folder/pick,
  file/write, file/listMatch, file/delete).
- Folders: per-folder color + icon (8-swatch palette, 8 icon presets),
  drag-reorder via HTML5 DnD with insert-line indicators, edit pencil
  on hover. New POST /folders/reorder + PUT /folders/{name}. Folder
  chip on cards inherits custom icon + color.
- Recently used: vault_entries.accessed_at + POST /entries/{id}/touch
  (debounced 2s), sidebar Tools entry showing top-10 by accessed_at.
- Encrypted attachments: per-entry file storage (5MB cap), AES-GCM
  with vault key, native Save As download, paperclip upload in
  slideover. New entry_attachments table + PM.Handler.Attachments.
- Password expiry: vault_entries.password_changed_at (conditional bump
  via SQL CASE only when ciphertext differs), passwordExpiryDays
  setting, "Aged" badge on cards + matching Filters chip.
- Recovery: Print button on generated code modal (A4 printable sheet
  via @media print, code in 32px monospace + instructions).
- Audit log viewer (sidebar Tools, GET /audit with pagination cursor).
- Plaintext CSV export + Filters dropdown with 9 predicates.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-21 23:12:06 +01:00
Zaki 63fac5b3b7 feat: secure notes + password history + custom fields + quick-win bundle
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>
2026-06-14 20:17:19 +01:00
Zaki 39406d712e feat: Ctrl+Shift+Q quick-search-fill + Edge browser directive
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>
2026-06-11 14:31:12 +01:00
Zaki f047fba9a3 feat: tray quick-search + privacy hardening + race fixes
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>
2026-06-10 19:35:37 +01:00