APK : le clair est permis par Android et trie dans TSyncClient.RefusalReason
(IsLocalHost : RFC 1918, 127.x, .local, .lan, nom sans point) - chacun pointe
vers son propre WebDAV sans IP connue a la compilation ; vers Internet, HTTPS.
PC : Authorization Basic envoye d'emblee, plus de 401 anonyme ni de PUT du
coffre envoye deux fois.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jeton "utilisateur:mot-de-passe" sur une URL ni Gitea ni GitHub = WebDAV,
authentification Basic. Fichier lu et ecrit tel quel ; l'ETag fait le sha
(If-None-Match, If-Match, 412 -> conflit). Clair autorise vers l'IP fixe du
poste seulement. Eprouve contre le WsgiDAV local : 304, PUT 204, puis
synchro du poste. Docs : CLAUDE.md, android-client/README.md (page eveillee
comprise).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Premier autofill apres des heures cachee : la page mettait 3 a 5 s a
repondre, alors que les drapeaux anti-gel de Chromium etaient appliques.
Un ExecuteJavaScript trivial toutes les 5 min, et tout de suite a la sortie
de veille ou au deverrouillage de session.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Apres un demarrage en tray, une connexion reussie puis un remplissage
repondait "Vault is locked" sans rien dire de plus. Chaque lockVault() porte
desormais sa cause (inactivite, session Windows verrouillee ou en veille, menu
du tray, PIN faux, geste manuel) avec l'heure ; sans verrouillage connu, le
message donne l'heure de chargement de la page - ce qui trahit une page
rechargee entre la connexion et le remplissage.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Un QR de texte, ou un QR dynamique qui encode un lien de suivi, etait lu puis
ecarte en silence : l'exe repondait "aucun QR 2FA trouve", l'APK continuait de
viser sans rien dire. Le texte d'un autre QR ne part toujours pas a la page ;
seul le fait qu'un QR a ete lu est signale.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
140.82.121.5 est une adresse de GitHub injoignable depuis ce reseau - verifie
par l'application ET par un navigateur, sur deux appareils. Un navigateur passe
a la suivante aussitot ; le poste attendait dix secondes puis abandonnait. Huit
secondes pour la poignee de main et trois essais, mais seulement quand la
requete n'a pas abouti : un 404 ou un 409 sont des reponses, pas des echecs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Un fichier nomme "vault-sync.json" annonce un coffre a qui parcourt la liste
des depots ; le contenu est chiffre, le nom ne l'etait pas. Les configurations
existantes gardent leur nom - c'est l'URL complete qui est enregistree - et le
QR du poste ne raccourcit plus que le nom par defaut courant, sinon le
telephone rallongerait vers un autre fichier.
Synchroniser coffre ferme ouvre d'abord le coffre : ce chemin depliait le
clavier de recherche au milieu d'une synchro.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Un jeton faux faisait repondre 401 a GitHub avant tout verdict : le refus
d'un depot public ne pouvait pas s'exprimer. Public = visible en anonyme.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
GitHub parle la meme API des contenus (base64, sha, corps du PUT) : meme
chemin, reconnu a api.github.com/repos/. User-Agent pose partout (GitHub
refuse sans). Un depot public est refuse avant toute lecture ou ecriture,
Gitea comme GitHub : l'historique garde tous les instantanes chiffres.
Telephone : github.com/O/R se convertit en URL d'API et inversement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
- 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>
Le scanner du telephone envoyait le texte a une recherche web (Google,
Chrome) : le jeton serait parti a un appui pres. Le lien a schema propre
n'a qu'une application pour l'ouvrir. Rien n'est enregistre avant Save.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
LICENCE-TESTS.md : cinq series. B eprouve ce qui doit tomber (chaine
quelconque, licence tronquee, charge utile trafiquee, autre poste, autre
paire de cles), C le risque symetrique et plus couteux — refuser un
client qui a paye (espaces colles, retour a la ligne, redemarrage
machine). D est dediee a la stabilite de l'empreinte, le defaut deja
rencontre. E delimite la portee de l'activation.
Le plan citait cmd://licence/clear sans moyen de l'appeler : un bouton
"Deactivate (debug)" au bas de Settings -> Licence, revele seulement
quand config.txt existe. Il donne au passage son unique appelant a
ClearLicence, qui etait du code mort.
Le document dit aussi ce qu'aucun essai ne prouve : rapiecer l'exe reste
possible, il n'y a pas de revocation, et le brouillage n'est pas du
chiffrement.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Porteur, courriel, date d'emission et Installation ID, lus dans la charge
utile deja verifiee. Rempli une fois au demarrage sur les deux chemins
(deja licencie, activation reussie) : rien n'y change pendant une
session. Section masquee hors navigateur hote.
Les champs sont echappes avant interpolation dans ExecuteJavaScript bien
qu'ils viennent d'une charge utile signee — signee ne veut pas dire
inoffensive.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
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>
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>
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>
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.
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.
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.
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.
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.
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.
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.
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>