Commit Graph

146 Commits

Author SHA1 Message Date
r-zakarya cd6e70449d feat(licence): ecran d'activation au demarrage
ensureLicence() en tete d'init(), avant tout le reste : un ecran
d'authentification qui s'affiche puis se fait recouvrir donnerait
l'impression que le coffre etait accessible.

L'ecran reprend les classes de l'ecran d'authentification — zero style
nouveau hormis un z-index. Le statut est demande deux fois avant de le
montrer : un demarrage a froid lent manquerait la fenetre de reponse, et
le silence se lirait comme un refus.

Une cle refusee ne dit pas pourquoi, et le journal note le verdict sans
jamais la cle.

ATTENTION : tant que LicencePublicKeyBlob est a zero, l'ecran est
infranchissable. Poser la cle publique avant de deployer.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 14:58:19 +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 c1d0eb3891 feat(desktop): sync to a private Gitea repo, chosen by the URL
Same discriminant as the Android client - "/api/v1/repos/" in the URL - so one
address tells both ends which transport to speak, and nothing new appears in
Settings beyond a placeholder and a warning.

The bridge stays shared. Three things differ, and they are confined: the header
is "token <PAT>" rather than Basic (Gitea answers a silent 401 to the other),
a PUT carries a JSON envelope with the file base64-encoded inside it, and the
connection test uses GET because the contents API does not answer HEAD.

The sha does the ETag's whole job. It comes back from the pull, rides in the
PUT body instead of an If-Match, and a stale one earns a 409 - handled exactly
like WebDAV's 412, by re-running pull->merge->push so the other device's
changes are folded in rather than clobbered. On a first push `sha` is OMITTED
rather than sent empty: Gitea reads its presence as "replace that exact
version". `branch` is omitted too, so the repo's default is used instead of a
name we would be guessing.

A failing PUT now reads its response body: Gitea explains a 409 or a 422 there,
and without it the error was just a number.

The test button checks the SHAPE of what came back, not only the status. A URL
one path segment off answers 200 with valid JSON that is not a file - which
would otherwise surface much later as "wrong sync password" and send you
looking on the wrong side. Four tests cover that parsing, including the
line-wrapped base64 Gitea sends and the array an accidental directory URL
returns.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 14:37:39 +01:00
r-zakarya ebcd75fd2e feat(backup): include the trash in exports and auto-backups
A backup that silently drops the trash is not a backup: export, empty the vault, import, and the trashed entries were gone for good with nothing said. Both containers now carry state.trashed with deleted/deleted_at, and deleted_at is preserved so a restore resumes the auto-purge clock instead of restarting it - hence its addition to the GET echo.

The sync snapshot deliberately does NOT: the trash is device-local, and re-publishing deleted rows is what produced the duplicate-uuid bug. The two identical export loops were folded into buildBackupEntry first, so the trash handling exists once.
2026-08-07 15:04:41 +01:00
r-zakarya 5ce18e7ab5 fix(prefs): stop pulling secret prefs into the renderer just to render the UI
A renderer dump taken after a quick-unlock, with nothing opened, contained the auto-backup password in cleartext - inside an ExecuteJavaScript string and in the DevTools protocol buffers - and it survived the auto-lock that wipes the vault key.

New prefs/has answers set-or-not without moving the value. The settings panel uses that; only the paths that encrypt a backup, test the WebDAV connection or run a sync fetch the secrets themselves.
2026-08-02 23:21:03 +01:00
r-zakarya 4a78dc2e90 fix(hotkeys): find the native dialog by enumerating the thread windows
GetLastActivePopup returns the form itself - the common Save dialog is not an owned popup - so the code that raises it never ran. It does live on our thread with the standard #32770 class, which EnumThreadWindows finds regardless of its owner.

Logging confirmed two things worth keeping: WM_HOTKEY is dispatched during the dialog modal loop, and IsWindowEnabled detects the state correctly.
2026-08-01 19:03:03 +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 1c0423680d fix(hotkeys): never touch the window while a native modal owns it
Ctrl+Shift+Q and Ctrl+Shift+A call RestoreWindowFromTray before JS can refuse, so a JS guard is too late by construction. Restoring the owner of a Save As dialog stranded it behind everything and the app looked frozen while it was only disabled.

NativeModalActive tests IsWindowEnabled - Windows disables the owner for the lifetime of the modal - and brings the dialog back to front via GetLastActivePopup instead of merely refusing.
2026-08-01 18:40:45 +01:00
r-zakarya 42d2d6fda5 fix(autofill): refusing a request must not move any window
The busy guard cancelled through the path that hands the foreground back to the captured target. During an export that pushed the native Save dialog behind the other application, leaving the app disabled by a modal the user could no longer find. refocus=0 separates refusing a request from ending a user interaction.
2026-08-01 18:30:00 +01:00
r-zakarya 5eb279ff31 fix(entries): 404 on PUT when the row no longer exists
The UPDATE matched nothing but the handler still answered 200 Updated, so the editor reported Saved for an edit that went nowhere - the entry had been deleted by a sync or a purge after the editor opened it. Silent data loss; the client already surfaces a failed save as an error toast.
2026-08-01 16:20:45 +01:00
r-zakarya 80711272c4 fix(tray): a -tray launch does take the foreground, so give it back
FMX shows its taskbar proxy before we hide it, so the launching console was left inactive. GStartupForeground, captured in the unit initialization before any window of ours exists, holds it. A Windows-startup launch is unaffected: the foreground is a shell window there, which the capture filters out.
2026-07-28 19:38:02 +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 a9e56c52b5 fix(autofill): send the window back to the tray when the picker is dismissed
Cancelling left the window on the desktop, though it had only been restored to
ask which entry to fill. Quick-search already hides itself on Esc; this makes
the picker symmetric. Applies to all three hotkeys — the capture and the hide
both sit on the shared BridgeAutofillRequest / autofill/cancel paths, so full,
password-only and TOTP behave identically.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 21:11:43 +01:00
r-zakarya 3c84bdca49 fix(autofill): return to the tray after picking in the multi-match picker
Only the quick-search path captured whether the window was tray-hidden and fed
it back as hide_after; JS never sends that parameter from the autofill picker,
so Ctrl+Shift+L pulled the window out of the tray and left it on the desktop.

Capture it in BridgeAutofillRequest instead, when the hotkey fires — by execute
time the picker has already restored the window and the state is gone. Same
rule as quick-search: tray-hidden only, a taskbar-minimised window is not sent
to the tray it never came from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 21:03:57 +01:00
r-zakarya e98acabbbd revert(winstate): drop the parked restored-size rect, it left a stub window
Widening rcNormalPosition while the window was down did remove the restore
flash, but SetWindowPlacement left a small stub frame in a corner of the screen
whenever the window was maximised — during the minimise animation and equally
once the window was already iconic. A permanent visible artefact is worse than
an ugly animation, so this goes back to the validated behaviour: correct final
state, flash still present.

The flash itself is an FMX bug, reproducible in an empty FMX app. Rationale and
the five rejected approaches are recorded in TEST_SCENARIOS.md I1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 20:51:28 +01:00
r-zakarya 17e817f8ab fix(winstate): park the restored-size rect so the FMX restore has nothing to grow
FMX drops the maximised state when Windows minimises the form externally —
reproducible in an empty FMX app. Windows then picks rcNormalPosition as the
restore target before it animates, so every reactive correction measured (queued,
inline in MINIMIZEEND, inside the restore's own WM_SIZE) landed after the frame
was already growing: the state ended correct and the flash remained.

Widen rcNormalPosition to the monitor work area while the window is down instead.
FMX still restores to normal, except normal is now full-screen, so nothing grows;
ReleaseNormalRect writes the real rectangle back once the window is maximised
again, keeping the user's own restored size intact.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 20:13:52 +01:00
r-zakarya b52cdc2d0b fix(winstate): apply the wanted window state inline, killing the restore flash
The MINIMIZEEND correction was posted through ForceQueue. Instrumenting the
hook showed the window state identical at the hook and one message-loop turn
later, so the queue guarded no race and only bought the normal-sized frame the
user saw. ApplyWantedWindowState now runs inline; the deferred pass stays as an
idempotent net.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-27 19:39:23 +01:00
r-zakarya 99b68c3e67 docs(startup): correct the rationale for the deferred server start
The comment justified deferring btnStartClick by the taskbar-proxy exposure
window, which the .dpr fix has since removed. Kept as-is per decision — it
works and reaching the tray sooner is desirable — but the comment now says
that, so nobody "fixes" it later against a premise that no longer holds.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 14:18:19 +01:00
r-zakarya ca23b88582 docs: section L — Windows startup (--tray)
L1 hidden start with no taskbar button, L2 the button coming back on restore,
L3 the normal launch (validated). Records why every fix placed inside the form
was powerless: the FMX proxy is shown before FormCreate is reached.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 14:12:31 +01:00
r-zakarya 8d03b74b6a chore: drop the -tray debugger run parameter from the project file
Committed by accident while testing the tray launch. Left in, every F9 from
the IDE would start hidden in the tray — the exact confusion the parameter is
worth warning about. It is a local debugging preference, not project config.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 14:09:29 +01:00
r-zakarya 04f52851fc fix(startup): use ApplicationHWND + WS_EX_TOOLWINDOW, restore it on show
Adopts the approach that actually works: ApplicationHWND (FMX.Platform.Win) IS
the taskbar proxy, so there is nothing to hunt for and it is valid before the
form exists. And hiding it was never enough — the taskbar had already drawn
the button; clearing WS_EX_APPWINDOW and setting WS_EX_TOOLWINDOW is what
removes it, since a tool window is never listed.

That style is permanent, which is why the button never came back after a
restore. TrayProxyShowInTaskbar puts it back, called from
RestoreWindowFromTray — the single choke point every tray restore already goes
through — so it lands before RestoreFromTray shows the windows and the style
takes effect on that show.

Removes the scaffolding this chase accumulated and that the real fix makes
pointless: the EVENT_OBJECT_SHOW hook, the polling worker, FindOurFMXAppWindow,
TrayHideNow with its four probes, the one-shot OnActivate hide, and the [tray]
tracing. ~4.8 KB of dead code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 14:06:19 +01:00
r-zakarya ab8186ef75 fix(startup): start the proxy watcher from the .dpr, before the form exists
Decisive observation: the taskbar button is already up before FormCreate is
entered. Everything I had tried lived inside the form, so all of it ran after
the button was drawn — the watcher included, which is why moving it around
never helped.

StartTrayProxyHider is now exported from UMainForm and called from the .dpr
right after Application.Initialize, before CreateForm. It polls every 5 ms and
hides our TFMAppClass proxy unconditionally (the button can be up before
WS_VISIBLE reads as set, which the previous IsWindowVisible check would have
skipped), stopping as soon as the tray startup completes so a user restoring
from the tray is never fought.

Raw Win32 only — FindWindowEx + ShowWindow, no FMX object touched at that
point, which is what made the earlier .dpr attempt crash.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 13:15:32 +01:00
r-zakarya 4f2aff9de4 fix(startup): hide the taskbar proxy from a worker thread
Why every previous attempt could not work: the main thread is BUSY inside
FormCreate and pumps no message, and that is exactly when FMX creates and
shows its taskbar proxy. A WinEvent hook, a timer, a queued call — none of
them can run in that window. Deferring the server start shortened the window
but did not remove it, because the proxy is created regardless.

A worker thread is the one thing still running while the main thread blocks.
It polls for our TFMAppClass window (matched on our own process id) and hides
it the instant it appears; ShowWindow is safe cross-thread. It stops as soon
as the tray startup completes, with a 10 s ceiling so it can never linger.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 12:37:51 +01:00
r-zakarya 17e48aa688 fix(startup): start the server AFTER the tray hide, not before
The visible thing is only the taskbar button — the form window is never shown
(form_visible_was=False throughout). That button belongs to the FMX proxy,
created partway through FormCreate, and FormCreate runs for ~1 s because it
opens the DB, binds the Indy server and kicks off WebView2. The thread pumps
no message during that second, so nothing could hide the proxy until it ended:
hooks and hide points alike were powerless by construction.

So stop trying to react faster and remove the wait instead: on a --tray launch
btnStartClick now runs inside the deferred block, after MinimizeToTray.
FormCreate returns in milliseconds, the proxy is hidden immediately, and the
server starts once nothing is on screen. FServer is constructed earlier
(line 295), so FServer.Active stays safe meanwhile. Normal launches keep the
original order untouched.

The EVENT_OBJECT_SHOW hook stays armed across the deferred start, so a proxy
re-shown during it is hidden again.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 12:33:26 +01:00
r-zakarya 8fa956f07f fix(startup): hide the FMX taskbar proxy the instant it is created
The instrumented run pinned it down: the FORM is never visible
(form_visible_was=False at all four hide points). What shows is the FMX
taskbar proxy — app=0 at 20.145, app=854682 at 20.974 — so it is created
partway through FormCreate and stayed on screen for the best part of a second
until the first hide that could see it.

A WinEvent hook on EVENT_OBJECT_SHOW, active only for the --tray startup,
hides any top-level window of ours the moment Windows shows it, and is torn
down once MinimizeToTray has run.

Two details that would each have broken it:
- WINEVENT_INCONTEXT, not OUTOFCONTEXT: the latter queues the callback until
  the thread pumps messages, which does not happen during FormCreate — exactly
  when the proxy appears.
- The pending flag is a unit-level var, not a form field: CreateForm assigns
  the MainForm global only after the constructor returns, so a callback firing
  during FormCreate could not have reached the form.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 12:28:29 +01:00
r-zakarya 283676224f fix(startup): hide at three points during FormCreate, and prove when it shows
The timeline says FormCreate itself runs for 1.02 s (45.509 → 46.532) while
the hide only landed at 46.663 — so if the window is visible during that
stretch, it accounts for exactly the second being reported.

TrayHideNow() hides the form AND the FMX taskbar proxy, and is now called at
three points: top of FormCreate, right after the browser is parented (which
forces the handle to exist, the likely moment FMX shows it), and at the end of
FormCreate — plus the existing on-activate one-shot.

It also logs whether the window WAS visible each time, which pins down exactly
where FMX turns it on instead of another guess.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 10:53: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 eec7bc4f80 fix(startup): revert the .dpr hide (AV) and hide on first activate instead
The .dpr version crashed with an EAccessViolation writing to 000000CE —
touching MainForm.Visible/Handle between CreateForm and Run is evidently too
early for FMX. Reverted; the app builds and runs as before.

Replaced with a one-shot OnActivate handler installed in FormCreate only for
--tray: it fires the instant FMX shows the form, hides it (FMX + Win32) and
unhooks itself, so it can never fight the user later. That runs far earlier
than the deferred MinimizeToTray, which waits for the message loop and hence
for the DB open, server start and WebView2 init — the second the window used
to spend on screen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 05:42:04 +01:00
r-zakarya 169cdddf9d fix(startup): hide the --tray window from the .dpr, where it sticks
Setting Visible := False in FormCreate wasn't enough — FMX re-shows the main
form after the constructor returns, so the window still appeared for ~1 s
during the Windows logon.

Do it in the .dpr instead, between CreateForm and Run: that is after FMX's own
show and before any message is pumped, so nothing ever paints. Backed by a
Win32 ShowWindow(SW_HIDE) so it holds whatever FMX decides. The handle is
still created, so WebView2 initialises normally and the vault is ready as soon
as the user clicks the tray icon.

The taskbar entry can still appear briefly; the deferred MinimizeToTray drops
it, along with adding the tray icon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 05:35:58 +01:00
r-zakarya e6a749465e fix(startup): --tray launch no longer flashes the window
With "Start with Windows" the window sat on screen for a second or two before
dropping to the tray. The hide was deferred with ForceQueue, which only runs
once the message loop is free — i.e. after the DB open, the HTTP server start
and the WebView2 init, all of which happen first.

Set Visible := False at the very top of FormCreate instead, before any of that
work, so there is nothing to show in the first place. The deferred
MinimizeToTray stays: it still adds the tray icon and drops the taskbar entry,
and doubles as the safety net if FMX shows the main form regardless — worst
case the behaviour is exactly what it is today.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 05:29:01 +01:00
r-zakarya e88dbf3ec8 fix(quick-search): Esc from the tray now closes AND returns to the tray
Two bugs in the same flow, both on the tray-originated path.

1. Esc did nothing until the search box was clicked: after the tray restore
   the WebView2 child does not hold the OS keyboard focus, so no keystroke
   reached the page at all — a DOM .focus() cannot fix that. Both quick-search
   entry points now call WebBrowser.SetFocus after restoring.

2. Esc closed the modal but left the app on screen. In fill mode
   closeQuickSearchModal fires cancelAutofill and then minimizeToTray, and two
   back-to-back window.location.href assignments COALESCE — the minimise was
   dropped. The copy path was unaffected because it never calls cancelAutofill,
   which is why this only showed up here. Deferred by a setTimeout(0), the same
   remedy CLAUDE.md already documents for quickunlock/store + pin/clear.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:25:28 +01:00
r-zakarya 2773cc444b fix(window): route every tray restore through one guard-clearing helper
Checking the hook's blast radius surfaced a real interaction: ExecuteAutofill
genuinely minimises the window (SW_MINIMIZE) before typing, which raises the
minimise guard. If the window is then HIDDEN to the tray rather than
un-minimised — the quick-search hide-after flow does exactly that —
MINIMIZEEND may never fire and the guard would stay raised for the rest of the
session, silently freezing the tracked window state.

All six FBridge.RestoreFromTray call sites now go through
RestoreWindowFromTray, which clears the guard first. RestoreFromTray replays
its own saved placement, so nothing is left for the guard to protect there.

MinimizeToTray itself is unaffected: it uses SW_HIDE, which raises no minimise
event at all.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 23:04:37 +01:00
r-zakarya 63dd7a296a chore(window): trim the winstate diagnostics now the fix is confirmed
The traces did their job: hwnd=657868 vs form=657842 proved Win+D minimises
the FMX taskbar proxy, not the form — which is why the earlier HWND filter
swallowed every event. Two full cycles then showed minimise start/end firing
and the re-maximise landing, with no stray "user resize" during the restore.

Keeps the two lines worth having: the hook install result (a silent failure
would be baffling) and the correction itself, which is rare.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 22:54:30 +01:00
r-zakarya 72fe01f513 fix(window): don't filter the hook on the form HWND
The hook installed but never fired: an FMX app owns TWO top-level windows —
the form and the taskbar proxy (the one FindFMXAppWindow digs out in
PM.Bridge) — and Win+D minimises the proxy. Comparing the event HWND against
the form handle rejected every event.

Dropped the comparison; the hook is already scoped to our process and WebView2
lives in separate ones, so any window event arriving here is ours. Logs the
install result and each event's HWND so a silent failure is visible next time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 22:50:26 +01:00
r-zakarya 7a19bdc13b fix(window): observe the minimise itself via a WinEvent hook
Third discriminator measured and rejected: the log shows the restore's resize
arrives with fg=True (22:44:08), so foreground ownership no more separates it
from a user gesture than the placement flags did.

FMX simply exposes nothing usable — no resize while iconic, and the restore's
resize is indistinguishable from the user's own. So stop inferring and observe
the transition at the OS level: SetWinEventHook on EVENT_SYSTEM_MINIMIZESTART
/ MINIMIZEEND, scoped to our process, WINEVENT_OUTOFCONTEXT (no injection,
callback on our thread).

Minimise start raises a guard that makes the resize handler ignore everything
until the restore settles; minimise end re-applies the state the user actually
left, in both directions. Hook failure is non-fatal — only the preservation is
lost. Unhooked in FormDestroy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 22:47:52 +01:00