Unified create/edit slideover
- openSlideOver(id) now accepts null for new entries. Same UI
(icon, name, site, user, password, TOTP, folder, tags) for both
create and edit. Drops the separate entry modal — no more "save
first, then add TOTP" two-step.
- "+ New" button, Ctrl+K → New entry, and Ctrl+Shift+A all route
through the slideover. Ctrl+Shift+A pre-fills the title with the
foreground window's name.
- Save button visible from the start in new mode (no dirty wait).
- Title shows mode unambiguously: cyan "+ New entry" vs
"Edit · <name>".
Custom icon upload (soIconField)
- 56×56 preview at the top of every slideover + Upload icon /
Remove buttons. Same POST /entries/{id}/icon endpoint as the
auto-fetch path. Validates type / size (64 KB cap matching server).
- Solves the case where DDG doesn't index a domain (self-hosted
apps, private sites): the user pastes any image and it sticks.
Favicon: privacy-first, DDG only
- Removed the direct-fetch fallback steps (3-5). Privacy stance:
zero DNS leak outside icons.duckduckgo.com. Domains DDG doesn't
cover stay icon-less until the user uploads a custom one.
- PM.Favicon.FetchFaviconDataUri takes an optional TFaviconLog
callback so UMainForm can stream per-step trace into LogLine for
diagnostics.
Fixes
- Slideover z-index 30 → 50. The topbar's backdrop-filter creates a
stacking context at z-index 40 which was clipping the slideover
header (title + close button hidden behind topbar).
- RestoreFromTray no longer un-maximises a maximised window when
called outside a tray-restore context (Ctrl+Shift+A, Ctrl+Shift+L
picker, app/focus cmd). SW_RESTORE on a maximised window reverts
to normal — now we only SW_RESTORE if IsIconic.
- "Show all"/"Show less" per-category state survives renderGrid
re-renders (healthExpanded map).
- "+ New" and dashboard "Fix" buttons stopPropagation so the
document-level click-outside handler doesn't close the slideover
they just opened.
- soDirtyCheck keeps Save visible while in new mode regardless of
diff.
- openSlideover → openSlideOver typo fix across all call sites.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
24 KiB
Password Manager — Architecture éclair
Desktop password vault Windows. Exe Delphi FMX embarquant WebView2 qui charge un frontend HTML/JS via Indy HTTP loopback (127.0.0.1:8765). 100 % offline, multi-utilisateur, chiffrement client-side (AES-GCM, clé dérivée PBKDF2-SHA256 600 k iter).
Stack
- Backend : Delphi 12, FMX, TMS FNC WebBrowser (Edge WebView2)
- HTTP : Indy
TIdHTTPServerbindé 127.0.0.1 only - DB : SQLite via FireDAC (
vault.db, gitignored) - Frontend : vanilla JS + CSS, pas de framework (
js/app.jsmonofile ~4000 lignes)
Build pipeline (CRITIQUE)
Toute modif index.html / js/ / css/ nécessite :
delphi-backend\assets\BuildAssets.cmd→ régénèreassets.res- Build Delphi (F9 ou MSBuild
PMServer.dproj)
Sans étape 1, l'exe embarque l'ancienne version des assets — le bug le plus courant après modif frontend.
Carte des fichiers
| Rôle | Path |
|---|---|
| Form principale, wire bridge, dispatch cmd:// | delphi-backend/UMainForm.pas |
| Bridge JS↔Delphi (clipboard, tray, hotkeys, autofill, focus, debug) | delphi-backend/Source/PM.Bridge.pas |
| HTTP server (ordre dispatch : router → embedded → static → 404) | delphi-backend/Source/PM.HTTPServer.pas |
| Routes API (regex match) | delphi-backend/Source/PM.Router.pas |
| Auth + sessions + CSRF | delphi-backend/Source/PM.Session.pas |
Migrations DB idempotentes (AddColumnIfMissing) |
delphi-backend/Source/PM.Database.pas |
| Static files disque (mort en prod, embedded gagne) | delphi-backend/Source/PM.StaticFiles.pas |
| Assets embedded dans .res | delphi-backend/Source/PM.EmbeddedAssets.pas |
| Quick unlock DPAPI (device-bound) | delphi-backend/Source/PM.QuickUnlock.pas |
Prefs key/value DPAPI (prefs.bin) |
delphi-backend/Source/PM.UserPrefs.pas |
| Single-instance mutex + broadcast | delphi-backend/Source/PM.SingleInstance.pas |
| Start with Windows (HKCU Run) | delphi-backend/Source/PM.AutoStart.pas |
| Favicon proxy (DuckDuckGo, async THTTPClient/WinHTTP) | delphi-backend/Source/PM.Favicon.pas |
| Handlers REST | delphi-backend/Handlers/PM.Handler.*.pas |
| Frontend complet | js/app.js |
| HTML racine | index.html |
| Styles | css/style.css |
Bridge cmd:// (JS → Delphi)
JS navigue vers cmd://action/sub?params → intercepté par
WebBrowserBeforeNavigate → HandleBridgeCommand.
Commandes connues :
clipboard/copy,clipboard/clear,clipboard/read(Paste custom menu)quickunlock/{store,get,clear,status}prefs/{get,set}?key=...(device-bound DPAPI key/value, voir plus bas)autostart/{get,set}?enabled=1|0(HKCU Run registry, "Start with Windows")favicon/fetch?host=X&reqId=Y(async via anonymous thread, callbackBridge.onFaviconResult(reqId, host, dataUri))autofill/{configure,hotkeys,execute,cancel}app/focus(ramène la fenêtre au premier plan, pour le picker)app/ready(page chargée → SetFocus WebBrowser + DOM focus auth input)app/theme?mode=dark|light(sync title bar + popup menus viaSetPreferredAppMode)audit(POST log d'action user-visible)entry/new-from-title(Ctrl+Shift+A → modal pré-rempli)
Retour Delphi → JS via WebBrowser.ExecuteJavaScript('Bridge.onX(...)').
Hotkeys globaux Win32
Ctrl+Shift+L: autofill complet (user + Tab + password)Ctrl+Shift+P: autofill password seul (forms step-2, unlock screens)Ctrl+Shift+A: new entry pré-rempli avec le titre de la fenêtre foregroundCtrl+Shift+D: toggle debug panel (uniquement siconfig.txtprésent)
Combos autofill configurables depuis Settings (synced en DB
via settings_json).
Debug mode
Fichier config.txt à côté de l'exe (tous champs optionnels) :
port=8765 # 0 ou absent = port aléatoire ephemeral (49152-65535)
debug=true # affiche PanelTop (Start/Stop, log memo)
require_token=false # désactive le token validation (curl/Postman direct)
require_process_check=false # désactive le PID-on-socket check
Sans config.txt, Ctrl+Shift+D ne fait rien (sécurité, panel jamais
exposé en prod).
HTTP server lockdown (anti-browser-direct)
Le serveur Indy n'accepte une requête que si elle a :
- Query param
?pmt=<token>(sur la 1ʳᵉ navigation) → Set-Cookie - OU cookie
pm_token=<token>; HttpOnly; SameSite=Strict; Path=/
Au start :
- Port aléatoire dans
[49152, 65535]si pas configuré - Token random 32-chars hex généré
- WebView2 navigate vers
http://127.0.0.1:{port}/index.html?pmt={token} - JS strip le
?pmt=...du URL bar viahistory.replaceState
Requête sans token valide → 404 (pas 403 pour ne pas révéler que le
serveur existe). Override via require_token=false.
Couche 2 : PID-on-socket process check
Sur chaque request, PM.ProcessLockdown :
GetExtendedTcpTable(winapiiphlpapi.dll) lookup le PID qui possède la connexion entrante (match sur Chrome'slocalPort+remotePort = our port)IsDescendantOfCurrentProcesswalk parent tree viaToolhelp32→ notre exe est-il ancêtre ?- Si non (curl externe, autre browser, etc.) → 404
Defense in depth : si quelqu'un connaît le token (leaked logs, mémoire),
il faut AUSSI exécuter le code dans notre lignée de process pour passer.
Override via require_process_check=false. Notre WebView2 (children
msedgewebview2.exe) passent naturellement.
Token masqué dans les logs
MaskAccessToken(url) dans UMainForm remplace ?pmt=<32 chars> par
?pmt=*** avant LogLine. Évite le leak via copie/upload du log.
Title bar + popup menus dark mode
PM.Bridge.ApplyTitleBarTheme(ADark: Boolean) fait 2 choses :
DwmSetWindowAttribute(DWMWA_USE_IMMERSIVE_DARK_MODE=20)→ title bar. No-op sur Win < 19044.uxtheme!SetPreferredAppMode(ordinal #135) +FlushMenuThemes(#136) — API privée stable depuis Win10 1809 utilisée par Explorer/Edge/Office. Themea automatiquement les popup menus (tray context menu inclus), scrollbars, tooltips. Valeurs :ForceDark=2,ForceLight=3.
Appelé au FormCreate + via cmd://app/theme?mode=dark|light quand JS toggle.
Single instance
Au démarrage, PM.SingleInstance.AcquireOrSignal :
- Crée un mutex nommé
Local\PMServer.SingleInstance.Mutex(namespaceLocal\= per-session, donc deux users Windows distincts peuvent chacun lancer le leur) - Si
ERROR_ALREADY_EXISTS:PostMessage(HWND_BROADCAST, WM_PMSHOW, 0, 0)oùWM_PMSHOW = RegisterWindowMessage('PMServer_ShowExisting')est un ID system-unique. Puis exit immédiat avantApplication.Initialize.
L'instance existante reçoit WM_PMSHOW dans son message-only window
(PM.Bridge.MsgWindowHandler) → déclenche FOnTrayRestore →
RestoreFromTray. AllocateHWnd crée une fenêtre top-level (pas
HWND_MESSAGE) donc elle reçoit bien le broadcast.
Tray icon
Shell_NotifyIcon(NIM_ADD) au constructor de TPMBridge, pas
au premier minimize → ic ône visible dès le démarrage, même si la
fenêtre est ouverte. NIM_DELETE uniquement au destructor.
MinimizeToTray ne touche plus à l'icône, juste à la visibilité de
la fenêtre + clipboard clear + balloon first-time.
Menu : Open / Lock vault / Quit (via TrackPopupMenu, themé par
SetPreferredAppMode ci-dessus).
Favicons (PM.Favicon)
Opt-in (state.faviconsEnabled, default OFF, synced via settings_json).
La SEULE feature qui sort sur le réseau côté Delphi (HIBP est côté JS).
- Source :
https://icons.duckduckgo.com/ip3/<host>.ico— proxy DDG, pas de tracking, retourne PNG 16-32 px. DDG-only par design : pas de fallback vers/favicon.icodirect (qui leakerait DNS à chaque domaine stocké). 2 essais : host complet puis SLD (chat.qwen.ai→qwen.ai). Pour les sites privés/self-hosted que DDG ne couvre pas, l'user upload une icône custom viasoIconFielddans le slideover THTTPClient(System.Net.HttpClient) qui wrappe WinHTTP sur Windows → TLS natif via le store de certificats Windows. Pas de DLLs OpenSSL à shipper (Indy aurait silencieusement fail sanslibcrypto-3.dll/libssl-3.dll). 5 s timeout, max 3 redirects, cap 64 KB. Toute erreur → return''- Magic-byte sniffing pour le MIME (PNG/JPEG/GIF/SVG/ICO)
- Cmd async :
TThread.CreateAnonymousThreadcar un GET HTTP bloquerait le main thread jusqu'à 5 s. Callback viaTThread.Queue→Bridge.onFaviconResult(reqId, host, dataUri) - Stockage : colonne
vault_entries.icon_b64 TEXT(nullable). Endpoint dédiéPOST /entries/{id}/iconpour ne pas forcer un PUT complet (qui re-PUT le password chiffré) - Bulk :
DELETE /entries/icons/allpurge tout (bouton "Clear cache") - Render :
entry-avatarcontient<img class="entry-avatar-img">siicon_b64, sinon fallback aux initials.onerrorrepasse aux initials si la data URI est corrompue - Auto-fetch après save d'entry, backfill via "Fetch missing" / "Re-fetch all" boutons dans Settings
Start with Windows (PM.AutoStart)
- Toggle dans Settings → "Start with Windows" (visible seulement quand
Bridge.active) - Mécanisme :
HKCU\Software\Microsoft\Windows\CurrentVersion\Run, valeur"PMServer"="<exe-path>" -tray(single dash —--trayne match pasFindCmdLineSwitch) - Per-user, pas d'admin requis. Visible dans Task Manager → Startup tab
- Toggle "on" = registered AND la valeur pointe vers notre exe courant (un old entry d'un exe déplacé lit comme "off" → user peut re-enable pour rafraîchir le path)
- CLI flag
-tray(FindCmdLineSwitch('tray', True)) dans FormCreate →FBridge.MinimizeToTrayviaTThread.ForceQueue(defer après la show initiale FMX pour minimiser le flash)
Device-bound prefs (PM.UserPrefs)
Problème résolu : le serveur HTTP bind un port éphémère aléatoire
(49152-65535) sur chaque start. localStorage est keyed par origin
(scheme+host+port) → reboot = nouvelle origine = localStorage
wipé. Pour les prefs qui doivent survivre (rememberedUsername,
etc.), on persiste via DPAPI.
%LOCALAPPDATA%\PMServer\prefs.bin— DPAPI-encrypted UTF-8 JSON{"key":"value",...}PM.UserPrefs.GetPref(key) / SetPref(key, value)Delphi-side- Bridge JS :
await Bridge.getPref(key)(Promise) /Bridge.setPref(key, value) - Callback :
Bridge.onPrefResult(key, value)posé par Delphi viaExecuteJavaScript. Resolvers stockés dansprefResolvers[key]avec timeout 2 s.
LoadAll retourne toujours un TJSONObject valide (jamais nil) —
défault = objet vide, remplacé seulement sur parse success.
Quick unlock — interactions
- Au boot,
init()callbridgeQuickUnlockStatus()et synchronisestate.quickUnlockEnabled+localStoragedepuis la source of truth DPAPI (corrige le décalage localStorage quand le port change). - Sur Windows lock / sleep (
BridgeSystemLock→Bridge.onSystemLock) :- Si
state.quickUnlockEnabled→ pas de lockVault. Le blob DPAPI gate déjà l'accès via le compte Windows, re-locker est redondant. Toast informatif affiché. - Sinon → comportement d'origine (
lockVault()).
- Si
- L'auto-lock par inactivité (
autoLockTimer) reste indépendant — il ignore le flag QU (opt-in utilisateur explicite via Settings).
Authenticator + TOTP tool
Sidebar Tools expose 2 items MFA :
- Authenticator (
state.view = 'authenticator') : grille de cards (chaque entry avec TOTP secret) — code 6 chiffres en gros + barre countdown.renderAuthenticatorGrid()décrypte tous les secrets upfront puis tick 1 s.authTickTimerclear sur lock / view change. Snap instantané à 100% sur reset de période (sinon CSS transition anime le saut backward → effet "freeze"). - TOTP generator (
openTotpTool()) : modal standalone. Paste base32 ouotpauth://URI → code live. Bouton "Generate" génère un secret base32 aléatoire 20 bytes (randomBase32Secret()). Rien n'est sauvegardé.
Code TOTP retourne { code, period, secondsLeft } — attention au nom,
pas remaining (utiliser t.secondsLeft).
Vault health dashboard
Sidebar Tools → "Vault health" (state.view = 'health'). Vue dédiée avec
un score 0-100 et 4 cards de catégorie :
- Weak :
computeStrength(plain) < 50 - Reused : groupes d'entries partageant le même plaintext (≥ 2)
- Old :
updated_at > 365 days - Pwned : depuis
state.hibpResults(n'apparaît que si HIBP actif)
computeHealthCache() décrypte chaque entry une fois et stocke dans
healthCache (module-level let, pas dans state pour ne pas polluer).
Invalidé sur lock, save d'entry, et bouton "Recompute".
Score : start 100 → -5/weak (cap -40), -10/reused (cap -30), -2/old (cap -20), -15/pwned (cap -50). Bandes : ≥80 Good (vert), ≥50 Fair (cyan), ≥25 At risk (orange), <25 Critical (rouge).
Bouton "Fix" par item → openEntryForFix(id) = openSlideover puis
click sur le bouton edit-password (fallback : juste ouvre le slideover).
Liste tronquée à 20 items par catégorie + count "+N more".
Sidebar sections collapsibles
Sections Folders, Tags, Tools ont chacune un .section-toggle
button (chevron + label + badge count) qui toggle .is-collapsed sur
la section parent. Body masqué via display: none sous .is-collapsed.
État persisté dans state.sidebarCollapsed = { folders, tags, tools },
synced via SYNCED_SETTING_KEYS (settings_json). Badges (#countFolders,
#countTags) restent visibles quand replié.
Hotkey verrouillé (UX)
autofillHandleRequest : si state.locked ou pas de cryptoKey →
Bridge.cancelAutofill() + Bridge.focusApp() puis focus le master
password input via DOM. Remplace l'ancien no-op silencieux qui laissait
l'utilisateur perplexe.
WebView2 / TMS settings — pièges majeurs
TTMSFNCWebBrowser expose des props qui semblent settables au FormCreate
mais ne sont effectivement appliquées qu'après l'init async du WebView2
sous-jacent. Set ces props dans WebBrowser.OnInitialized, pas FormCreate :
EnableContextMenu := False→ kill le menu Edge natif (Inspecter, Importer mots de passe…)EnableShowDebugConsole := False→ kill DevToolsEnableAcceleratorKeys := False← NE PAS METTRE : casse aussi Ctrl+C/V/X/Z/A/K dans les inputs
Le blocking F12 / Ctrl+Shift+I/J / Ctrl+U se fait côté JS (keydown preventDefault) en complément.
Custom right-click menu
Le menu natif est désactivé via TMS. On gère un mini-menu JS dédié sur
right-click dans les inputs : installCustomContextMenu() → Cut / Copy /
Paste / Select All. Paste utilise Bridge.readClipboard() (sans prompt
WebView2 contrairement à navigator.clipboard.readText()). stopPropagation
sur les click items sinon le slideover se ferme (chip retiré du DOM
avant que le doc click handler check .closest('.slideover')).
Browser shortcuts bloqués (keydown JS)
Bloqués via preventDefault :
F12,Ctrl+Shift+I/J,Ctrl+U— DevTools / View sourceCtrl+J— Downloads overlay (le moins évident, déclenche un popup Edge)Ctrl+H/S/P/T/N/R,F5— History / Save / Print / New tab+win / ReloadCtrl+Shift+N/W/Delete— Incognito / Close window / Clear data
Conservés : Ctrl+C/V/X/A/Z (édition), Ctrl+F (find in page), Ctrl+0/+/-
(zoom accessibility), Ctrl+K (notre command palette), Ctrl+Shift+L/P/A/D
(nos hotkeys).
Pagination
State : pageSize (10/25/50/100, default 25, synced via settings_json)
currentPageruntime-only. Active sur les 3 view modes (cards/list/ table) si > 10 items. Position top (au-dessus du grid/table).
CSS : .pagination { grid-column: 1 / -1 } pour span full width en cards
view (sinon ça occupe une slot de card).
Reset currentPage = 1 sur : search, sort, view (folder/fav/tag),
viewMode change, pageSize change. Render via renderPagination(total, totalPages)
computePageList(current, total)avec ellipsis intelligente (1 … 4 5 6 … 12).
Flash animation (nouvelle entry)
flashEntry(id) appelée après saveEntry success + duplicateEntry :
setTimeout(0) → querySelector('.entry-card[data-id],.entry-row[data-id]')
→ scrollIntoView({block:'center'}) + classe is-flash 2.5s
(@keyframes entryFlash pulse cyan).
Résout le cas "j'ai ajouté un mot de passe avec tri A-Z, où se loge-t-il ?" — scroll + pulse trouvent la nouvelle entry dans la grille triée.
Settings sync
Per-user blob JSON dans users.settings_json, exposé via GET/PUT /settings. Synced : theme, autoLock, viewMode, sortBy/sortDir, hibp,
hotkeys autofill, etc. Device-only (localStorage seulement) :
quickUnlockEnabled (DPAPI lié au compte Windows), autofillEnabled
(toggle hotkey Win32), rememberedUsername (auth screen autofill local).
Quick Unlock
DPAPI blob à %LOCALAPPDATA%\PMServer\quickunlock.bin (tied to Windows
user, pas à l'emplacement de l'exe — survit au déplacement).
Cold-start restore : tryQuickUnlock ne check pas localStorage
comme source of truth (WebView2 user-data folder est relatif à l'exe →
move = localStorage perdu). Le fichier DPAPI seul gouverne. Si présent :
- Décrypte blob → restore key
- Re-login fresh via
/loginavecverifier = bytesToHex(rawKey)(le token stocké dans le blob peut être expiré ; on en récupère un nouveau à chaque cold-start) - Resync
localStorageà'1'pour que Settings affiche "Disable"
Recovery code (refactor 2026-05)
- 5 uses par code (compteur
remaining_usesdansrecovery_keys) - Décrémenté à chaque redeem. Si tombe à 0 → row deletée
- Row deletée aussi sur
successful change-master-password - Workflow attendu : recover → set new master pw (consomme le code) → regenerate un nouveau code
State justRecovered : flag JS posé après redeem, cleared sur
- successful master pw change (le code est consumed)
lockVault(lock = perte de contexte recovery)doLoginsuccess (login normal avec pw = sortie du mode recovery)
En mode recovery, le modal "Change master password" masque le champ
"Current master password" ET retire son required (sinon HTML5 form
validation bloque silencieusement le submit). Le verifier "current" est
dérivé du state.cryptoKey en mémoire (qu'on vient de recover).
Autofill pipeline
Workflow utilisateur (important)
L'utilisateur doit cliquer le champ cible AVANT de presser le hotkey. Le code ne fait plus de "click au centre de la fenêtre" pour deviner le champ — ça détruisait le focus sur les forms qui n'étaient pas centrés (Gitea login, etc.).
Ctrl+Shift+L(full) : click sur username field → hotkey → user+Tab+pwdCtrl+Shift+P(password only) : click sur password field → hotkey → pwd seul
Pipeline Delphi
RegisterHotKeyCtrl+Shift+L/P sur message-only windowWM_HOTKEY→ captureGetForegroundWindow+ titre, fire callback- JS reçoit
Bridge.onAutofillRequest(title, kind)→ strip browser suffix (- Google Chrome, etc.) → matchingstate.entries.site - Single match →
Bridge.executeAutofill(user, pwd)direct Multi-match →Bridge.focusApp()+ picker modal → user pick → execute Note :closeAutofillPicker(false)quand user pick (skip cancel cmd qui clearraitFAutofillTargetHWND) - Delphi
ExecuteAutofill:- Si app foreground →
SW_MINIMIZE(laisse la place au target) ForceForegroundWindow(target)avecAttachThreadInput+ poll jusqu'àGetForegroundWindow == target(max 600ms)WaitForModifierRelease(sinon Ctrl+Shift restent down → toutes les frappes deviennent Ctrl+Shift+X)Sleep(120)settle- Pas de click parasite — le user a déjà focusé le bon champ
Ctrl+A + Delavant chaque champ (clear contenu existant)- SendInput :
Ctrl+A+Del → username → 200ms → Tab → 200ms → Ctrl+A+Del → password
- Si app foreground →
Matching titre → entry
- Strip suffixe browser via
BROWSER_SUFFIX_RE(Chrome, Firefox, Edge, Brave, Opera, Vivaldi, Safari, Tor, Arc) avant toute comparaison - Score = max sur 3 champs :
entry.title(display name, score 1500+),entry.sitehostname full (1000+), SLD (500+) - Pour SLD courts (
x,qq), match avec word boundaries entry.titlepriorisé → si utilisateur stockesite = "git.example.com"(hostname), mettre aussititle = "Gitea"(brand) couvre le browser title sans ambiguïté
Contraintes connues autofill
- UIPI : SendInput vers process élevé bloqué. Notepad++ admin, regedit → autofill silencieusement no-op. Tester avec apps non-élevées.
- Chrome password manager : si Chrome a des credentials sauvegardés pour le domaine, il peut racer avec notre SendInput et écraser notre username pendant le Tab. Workaround : user désactive Chrome PM par site (clic-droit password field → "Never save passwords for this site")
- Gitea-like forms : tabindex explicite +
autocomplete="current-password"→ Chrome PM s'active. Désactiver pour ce domaine ou utiliser Ctrl+Shift+P - Title-based matching : si l'utilisateur stocke
site = "git.ai-agents4you.com"(hostname) mais le titre browser n'a que la marque ("Gitea"), pas de match. Convention : préférersite = "Gitea"(brand) pour les sites brandés. URL hostname-only fonctionne pour les sites dont le titre contient le hostname (rare).
Contraintes / pièges connus
- UIPI : process non-élevé ne peut pas SendInput vers process élevé
(Notepad++ admin, regedit, …). Solution future : manifest
uiAccess=true+ exe signé EV + installé dansC:\Program Files\. - Assets embedded > static dans le dispatch HTTP : modifier JS sans
rebuild
assets.resn'a aucun effet sur l'exe. - WebView2 mange tous les keydown : ne pas utiliser
FormKeyDownpour des raccourcis globaux → toujoursRegisterHotKey. - FMX
FormCloseQuery: minimize-to-tray sauf siFQuitting=True(set par le tray menu "Quit" uniquement). SetForegroundWindowWin10/11 : règles strictes anti-stealing → nécessiteAttachThreadInputpour cross-process.updated_atest bumpé à chaque PUT entry → sort par défaut =nameasc pour que modifier une entry ne change pas sa position.- TMS render-time vs DOM-attachment : pattern courant — fonctions
qui rendent un widget (
soTagsField,soTotpField) appellent un helper (renderSoChips,startTotpTick) qui fait$('#id')sur un élément pas encore dans le DOM. Solution : render inline OUsetTimeout(fn, 0)pour deferrer jusqu'au tick suivant. - HTML5
required+display:none: un inputrequiredcaché bloque silencieusement le form submit. Toggle aussi.required = falsequand on cache (cf. modal recovery). - WebView2 user-data folder est relatif à l'exe : déplacer l'exe =
nouveau folder =
localStorage/sessionStorageperdu. Pour ce qui doit survivre au move, stocker via Delphi (DPAPI, fichiers user). - Port éphémère = nouvelle origin = localStorage wipé chaque launch :
cause distincte du point précédent (l'exe ne bouge pas, mais le port
change). Mêmes conséquences : pour tout pref qui doit survivre au
reboot, passer par
PM.UserPrefsviaBridge.getPref/setPref. Le fix port-fixe est possible (port=Ndansconfig.txt) mais on perd l'aléatoire qui complique le scan externe. - Modifier les
Enable*props TMS enOnInitialized, pas FormCreate.
Conventions code (héritage sessions précédentes)
- Pas de commentaires multi-lignes pour chaque fix : préférer noms
de variables/fonctions descriptifs (
OwnFormHwnd,EnsureTargetThreadHasKeyboardFocus,FocusSettleDelayMs) - Migrations DB : toujours via
AddColumnIfMissing(idempotent) - Nouveau setting UI : ajouter à
SYNCED_SETTING_KEYS+ handler dansloadServerSettingsswitch case + listener change qui appellesaveServerSettings() - Tout new bridge cmd : handler dans
UMainForm.HandleBridgeCommand, log viaLogLinepour traçabilité
Audit / docs
security-issues.mdà la racine : audit sécu legacy (api.php)- Ce CLAUDE.md = source de vérité pour l'architecture courante
Git
- Pas de remote configuré (dev local)
vault.db,*.db-shm,*.db-wal,delphi-backend/Win32/,*.dcu,assets.resgitignored- Messages commit :
type(scope): descriptionstyle conventional