Commit Graph

85 Commits

Author SHA1 Message Date
r-zakarya 508d5345bf feat(fillbar): option fond transparent (cle de couleur), l'opacite reste sur les boutons
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:12:37 +01:00
r-zakarya 9a09cc5d29 feat: survol a l'accent sur la barrette, grande fleche de retour dans l'APK
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 14:16:45 +01:00
r-zakarya 891abf7a9d fix(fillbar): l'onglet Fill bar se met a jour quand la barrette change par clic droit ou par le tray
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 11:19:00 +01:00
r-zakarya ba06d4584e feat(fillbar): bouton Settings sur la barrette (icone roue ou texte), reglable comme les autres
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 11:14:41 +01:00
r-zakarya 9947ce5718 feat(fillbar): Fill bar settings... au clic droit, ouvre l'onglet par-dessus l'entree en cours
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 11:08:52 +01:00
r-zakarya d7a8b4ddff feat(fillbar): onglet Fill bar dans les reglages, icones ou texte, boutons Add et Search
- boutons choisis (Full, Password, 2FA, Add current window, Search, Open),
  style icones (Segoe MDL2 Assets) ou texte, opacite au repos, allumee ou non
- clic droit sur la barrette : Show icons / Show text en plus de l'opacite
- info-bulles sur chaque bouton
- Add et Search reprennent les corps extraits de Ctrl+Shift+A et Ctrl+Shift+Q

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 10:24:26 +01:00
r-zakarya f817a6df43 feat(sync): QR des reglages sur le PC, Paste settings sur le telephone
Le poste genere le QR (DelphiZXingQRCode, Apache 2.0) depuis ses prefs :
URL + jeton, jamais le mot de passe de synchro. Le telephone lit le texte
copie par l'appareil photo et remplit les champs ; Save valide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 18:03:00 +01:00
r-zakarya d41dbfbb3a fix(tray): relancer l'exe ramene bien la fenetre au premier plan
Lancee, fermee par la croix (tray), puis relancee : la fenetre revenait
derriere les autres, seule son icone dans la barre des taches. Mesure :
visible, non minimisee, pas au premier plan.

La 2e instance, qui recoit le double-clic, reveille la 1re par un
message ; mais la 1re n'a recu aucune entree, et Windows lui refuse
SetForegroundWindow. La 2e cede maintenant son droit
(AllowSetForegroundWindow) avant d'envoyer le message.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 06:20:17 +01:00
r-zakarya d5234e1390 fix(licence): refus de la 2e couche indiscernable d'une route inconnue
Le 404 a corps vide etait remplace par la page HTML d'Indy, distincte du
404 JSON d'une route inexistante : on pouvait lister les routes reelles.
La 2e couche renvoie maintenant exactement le meme JSON.

Ajoute un exe d'essai qui saute l'ecran d'activation, pour eprouver la
2e couche depuis l'application. Define de compilation
LICENCE_TEST_SKIP_SCREEN, jamais un reglage : un interrupteur dans
config.txt ferait sauter la 1re couche sans rapiecage. Build dedie,
.dcu a part ; verifie que seul l'exe d'essai porte le define.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 05:36:28 +01:00
r-zakarya a7af9af47e fix(build): lier SQLite dans l'exe, qui chargeait sqlite3.dll du PATH
Sans FireDAC.Phys.SQLiteWrapper.Stat, FireDAC charge sqlite3.dll au
demarrage. Sur ce poste ca marchait quand meme : le dossier bin
d'Embarcadero, qui en contient un, est dans le PATH. Sur une machine
sans Delphi, l'exe refusait de demarrer.

C'est le Win64 qui l'a revele — la DLL du PATH est 32 bits — mais le
Win32 livre etait casse de la meme facon chez quiconque n'a pas Delphi,
paquet prepare pour un tiers compris.

Prouve avec un PATH reduit a Windows : avec l'unite SQLite s'ouvre
(3.42.0), sans elle l'erreur exacte. Lire la table d'import ne
suffisait pas, le chargement est dynamique.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 23:15:42 +01:00
r-zakarya 1fa5113c02 feat(licence): deuxieme couche, l'API refuse sans licence valable
L'ecran d'activation ne consultait qu'un branchement : le rapiecer
ouvrait tout, l'API ne demandant rien. Chaque route de PM.Router passe
maintenant par PM.LicenceGate.ApiLicenceOk avant son handler.

Aucune fonction de decision partagee avec PM.Licence — sinon un seul
rapiecage ouvrirait les deux couches. La verification est refaite de
bout en bout, appels CNG redeclares compris ; seules la cle publique et
le nom de la pref sont partages, parce que ce sont des donnees.

Branchee dans DispatchRequest : les fichiers de l'ecran d'activation
passent par TryServeEmbedded, non garde, sinon l'ecran ne se chargerait
plus. Un oui est retenu pour le processus, un non jamais.

Le risque etait qu'elle refuse un client licencie. Auto-test sur un
vecteur lie a une empreinte fixe, sens accepte en premier ; et accord
verifie en conditions reelles sur ce poste : IsLicensed = TRUE,
ApiLicenceOk = TRUE.

Documente aussi le piege du jour, sans lien avec ce code : des .dcu TMS
x64 deposes a cote des sources cassaient le build Win32, et la sortie
redirigee vers E: faute de place sur Z:.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-16 22:55:33 +01:00
r-zakarya fc945ac6ab fix(licence): nouvelle paire, et la privee ne nait plus dans le depot
La premiere privee a ete supprimee sans copie : plus aucune licence
nouvelle n'etait emettable. Regeneration + reemission, indolore
aujourd'hui parce qu'une seule licence circulait.

licence.ps1 ecrit desormais la cle dans %USERPROFILE%\Documents, hors du
depot. Elle vivait dans tools\, ou elle n'attendait qu'un menage : le
.gitignore la protegeait du depot, rien ne la protegeait de son
proprietaire.

Verifie : la licence de la nouvelle paire passe, celle de la paire
perdue tombe — la regeneration a bien remplace, pas ajoute.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 17:50:14 +01:00
r-zakarya 427d294b22 feat(assets): brouiller les blocs embarques dans l'exe
Le frontend etait lisible dans l'onglet Image de Process Explorer, avec
les commentaires. BuildAssets.ps1 brouille chaque fichier avant brcc32,
PM.EmbeddedAssets le debrouille au moment de servir.

C'est de l'opacite, pas du secret : la cle est dans l'exe. Ca supprime
strings et l'onglet Image ; la memoire, un debogueur et la reponse HTTP
rendent toujours le texte. D'ou XOR plutot qu'AES — a securite egale
(nulle contre qui a l'exe), dix lignes, et aucune force affichee qu'on
n'a pas.

Une magie PMA1 prefixe le clair : sans elle, deux constantes qui derivent
serviraient du binaire au navigateur, ecran blanc sans trace.

Verifie en comparant les flux des deux cotes bloc par bloc, puis par
aller-retour sur app.js, index.html et style.css. Le frontend ne se
trouve plus dans le binaire.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 17:17:15 +01:00
r-zakarya 61ec5ff7a1 fix(licence): l'empreinte ne depend plus de l'ordre de demarrage
BoardUUID s'appuyait sur un COM initialise par quelqu'un d'autre. Appele
apres WebView2 il lisait le vrai UUID de carte mere, appele avant il
echouait et retombait sur MachineGuid : deux empreintes pour une seule
machine, et une licence liee a l'une que l'autre refuse. Il suffisait que
licence/status parte avant l'init du navigateur pour verrouiller
l'utilisateur hors de son coffre.

CoInitializeEx est desormais appele sur place. Pas de CoUninitialize
symetrique : essaye, il rend une violation d'acces — les mandataires WMI
et le cache de type-info de ComObj se liberent plus tard, dans un
appartement qu'on vient de fermer.

Verifie avec un harnais qui n'initialise PAS COM : il rend maintenant la
meme empreinte que l'application.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 16:00:50 +01:00
r-zakarya 988694b20d feat(licence): refuser les licences non liees a un poste
Une cle flottante etait sortie. Un schema hors ligne n'a pas de
revocation — rien ne peut aller dire a un exe installe qu'une cle ne vaut
plus — donc la seule prise est de cesser d'accepter toute la classe.

MachineMatches exige desormais une empreinte, et -Floating disparait du
script : il ne peut plus fabriquer une cle que l'exe rejetterait.

Effet de bord assume : plus de licence de site, et un poste sans
empreinte exploitable n'est plus licenciable du tout — mieux qu'une
empreinte degeneree qui ouvrirait partout.

Verifie : liee a ce poste acceptee, liee a un autre refusee, les deux
flottantes deja emises refusees.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 15:38:07 +01:00
r-zakarya 405ac7fac4 feat(licence): lier la licence au poste (carte mere + disque)
C'etait la decision prise, et je l'avais mal lue : "pour le moment 1"
repondait a la question 1, pas "une seule licence".

PM.MachineId melange l'UUID de la carte mere (WMI) et la serie du volume
systeme, puis n'en publie que 16 hexa : l'empreinte se recopie dans un
courriel, l'UUID brut n'a pas a circuler. Un UUID nul ou tout a FF est
rejete — VM et images clonees l'annoncent, et deux postes tomberaient
alors sur la meme empreinte.

Le champ m est compare APRES la signature. m absent = licence flottante,
acceptee : seule la cle privee peut en emettre, et le champ ne peut pas
etre retire sans casser la signature. Cote script -Machine est
obligatoire sauf -Floating explicite.

L'ecran d'activation affiche l'Installation ID : une licence liee ne peut
plus etre emise avant l'installation.

Verifie sur ce poste : liee a lui acceptee, liee a un autre refusee,
flottante acceptee.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 15:16:35 +01:00
r-zakarya d85f01ede2 feat(licence): la cle publique de l'editeur est posee
L'ecran d'activation devient franchissable. Verifie en executant l'unite
contre la vraie cle : licence emise acceptee et charge utile lue, licence
signee par une AUTRE paire refusee — c'est celle-la qui prouve que la cle
tient reellement la porte — charabia et chaine vide refuses.

La cle privee reste hors du depot (gitignoree) et doit etre deplacee dans
un coffre : qui la detient fabrique des licences.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 15:00:09 +01:00
r-zakarya 960d2b388a feat(licence): verification hors ligne d'une licence signee
PM.Licence verifie une chaine charge-utile.signature contre la cle
publique de l'editeur, via BCryptVerifySignature. Aucun ASN.1 : la
signature du script est deja en r||s brut et les coordonnees se rangent
telles quelles dans un BCRYPT_ECCPUBLIC_BLOB.

La cle publique est livree A ZERO — tant que la sortie de licence.ps1
-Init n'y est pas collee, aucune licence n'est valide. C'est le defaut
sur : un blob par defaut qui accepterait quelque chose serait pire.

IsLicensed REVERIFIE la licence rangee plutot que de lire un drapeau.
La signature est verifiee AVANT que la charge utile soit analysee.

SelfTestLicence a l'initialization rejoue un vecteur fige signe par une
paire de test : valable accepte, un octet retourne refuse. Verifie en
executant l'unite, pas seulement en la compilant.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 14:44:55 +01:00
r-zakarya bf9b62a541 feat(server): un port occupe ne tue plus le demarrage
Le lien n'etait garde par rien : un port pris levait, et l'application
s'ouvrait sur une boite d'erreur. Trois essais desormais - le premier
honore le port de config.txt, que l'utilisateur a peut-etre choisi
expres, les suivants prennent un ephemere.

SE LIER EST LE TEST. Sonder le port avant serait plus faible - un service
qui ecoute sans repondre passerait inapercu - et sujet a une course : le
port peut etre pris entre le sondage et le lien. Chaque deplacement est
journalise, un serveur qui change de port en silence etant pire qu'un
serveur qui refuse de demarrer.

Trouve en chemin : Randomize n'est appele NULLE PART, donc le port
"aleatoire" de port=0 etait le meme a chaque lancement - l'aleatoire cense
compliquer un balayage exterieur n'existait pas. Il vient maintenant de
RandomBytes, donc de BCryptGenRandom, deja utilise pour le jeton d'acces.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 08:45:32 +01:00
r-zakarya 77688a5af2 chore(autofill): le journal dit par quel geste on est entre
La barrette et le raccourci se rejoignent des FireAutofillRequest, donc
tout ce qui suit etait journalise sous "hotkey" - y compris un
remplissage declenche a la souris. Un diagnostic de lenteur qui ne sait
pas par ou l'on est entre cherche au mauvais endroit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 19:36:20 +01:00
r-zakarya da74a675be chore(autofill): mesurer ou passe le temps d'un remplissage
Un autofill lent apres une periode d'inactivite peut l'etre pour des
raisons sans rapport qui se ressemblent parfaitement de l'exterieur : le
moteur WebView2 qui se reveille, la prise du premier plan, ou l'attente
de la relache des modificateurs. Elles se corrigent a des endroits
opposes, donc on les separe au lieu de deviner laquelle accuser.

Trois marques : le temps que met la PAGE a repondre au raccourci, le
total, et le detail des deux attentes internes.

Premiere mesure : 13 ms de reponse apres 23 minutes en tray, total
635 ms. Le renderer ne dormait donc pas - l'hypothese la plus evidente
etait fausse - et 635 ms est le cout nominal de la sequence, faite de
pauses fixes (80 + 120 + 60 + 200 + 200). Le cas lent reste a attraper.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 19:31:03 +01:00
r-zakarya f8b7ffe379 refactor(autofill): "Fill bar" plutot que "Lazy mode"
Le premier nomme la chose, le second decrivait l'utilisateur. Le signe
qui a tranche : l'ancien libelle avait besoin d'une parenthese pour se
faire comprendre - "Lazy mode (fill with the mouse)" - et c'etait elle
qui portait le sens.

Seul le libelle change. Les identifiants internes gardent "lazy" -
FLazyWnd, LAZY_*, et surtout les prefs lazyOn / lazyPos / lazyAlphaPct :
renommer ces cles perdrait les reglages deja poses sur la machine.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 01:03:06 +01:00
r-zakarya 35d0d8ddf3 feat(autofill): la barrette revient allumee si elle l'etait
Pref DPAPI lazyOn, reprise par RestoreLazyBar depuis FormCreate - apres
le theme, parce que la barrette se peint elle-meme, et apres le cablage
de OnTrayRestore, dont depend son bouton Open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 14:26:18 +01:00
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