Modele "commands" : "# titre" ouvre un groupe, chaque ligne est une
commande. Le corps reste du texte (aucune colonne, sync et APK inchanges).
Panneau : liste avec Copier. Recherche rapide : la note s'ouvre sur sa
liste filtrable ; Entree tape la commande sans Entree et sans vider le
champ, Ctrl+Entree copie, Esc revient. Decoupage teste.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fleches dans l'en-tete du panneau et Alt+fleches, comme un navigateur :
un clic par erreur dans une liste filtree ne force plus a refaire la
recherche. Historique en memoire (50), vide au verrouillage ; la navigation
repasse par openSlideOver et sa confirmation de modifications non
enregistrees. Logique pure testee (history.test.js).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Off = vk 0, non enregistre : la combinaison reste libre pour les autres
applis. Combinaisons retenues en pref DPAPI et rejouees au demarrage, pour
qu'un raccourci coupe le reste coffre ferme. Esc annule enfin la capture au
lieu de fermer les reglages.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Le champ de recherche gardait la largeur de son texte d'aide (min-width par
defaut) et poussait l'interrupteur et Esc hors du panneau. Le texte d'aide
parlait aussi de copier alors que Fill est souvent actif : il dit "Search..."
tout court. Et le bouton d'une note precise, en mode Fill, qu'une note n'est
jamais tapee.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Ouverte par Ctrl+Shift+Q ou la barrette, la recherche remplissait toujours ;
copier restait impossible alors que le remplissage echoue vers une application
lancee en administrateur, ou quand le gestionnaire de Chrome s'en mele. Sans
cible (tray, palette), l'interrupteur n'apparait pas : remplir n'aurait rien a
viser. Le choix est retenu en pref DPAPI, les verbes des boutons et l'aide
suivent.
Co-Authored-By: Claude Opus 5.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 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>
Les onglets etaient visibles par defaut puis masques a la reponse de
/ping : Register restait affiche quelques millisecondes. Ils naissent
maintenant invisibles (visibility, place reservee) et ne sont reveles
qu'une fois la reponse connue — licence valide, ils apparaissent sans
rien decaler ; licence refusee, Register n'est jamais montre.
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>
Synchroniser est une ACTION, pas un reglage. Elle n'existait qu'au fond
du panneau des reglages, a cote de l'URL et du jeton : on l'y configure
une fois, on la declenche tous les jours.
Sans URL, sans "Enable sync" ou sans mot de passe de sync, l'appui ouvre
les reglages ET DESCEND jusqu'a la section - un bouton qui ne peut
qu'echouer ne vaut pas mieux que pas de bouton, et repondre "Sync not
configured" laisse chercher ou reparer. Le panneau n'a pas d'onglets,
c'est une colonne longue : le battement dit lequel des blocs on est venu
chercher.
L'etat est lu SANS les secrets - hasEncPwd dit que le mot de passe existe
sans le lire, ce que loadSyncConfig documente : les tirer les planterait
dans le renderer pour la vie du processus.
L'entree disparait sans le pont, comme la section : le frontend PHP n'a
pas de sortie reseau.
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>
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>
openEntryModal had zero callers — the slideover editor replaced it — but the
whole cluster still shipped: the #entryModal markup, saveEntry (wired to the
form submit), closeEntryModal, populateFolderSelect, updateEntryStrength, and
the entire tag-chip subsystem (editingTags, renderChips, addTag, the suggest
dropdown) plus their init wirings and the Esc-chain branch.
saveEntry was a latent hazard: still bound to #entryForm submit, it did a
PARTIAL PUT (no kind / totp_secret / custom_fields / template) — the exact
silent-data-loss bug moveEntryToFolder documents. Unreachable today, but one
stray call away from wiping fields.
Removed JS + HTML + the orphaned .chip-suggestion CSS. The generator is shared
with the slideover — kept; its dead 'entry' target branch collapsed to
slideover-only. 322 lines gone, tests green.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The show/hide-columns gear only makes sense in table view but sat in the
global toolbar (needing applyColsWrapVisibility to hide it elsewhere). Build
it inside the actions-column header cell in renderTable instead, with inline
handlers (recreated each render, so no dependence on once-wired listeners on
a static node). Removed #colsWrap from the toolbar + the now-obsolete
applyColsWrapVisibility and its callers; kept the menu id so
renderTableColsMenu still populates it, and a live-queried outside-click close.
Added .cols-wrap CSS (was an inline position:relative on the old node).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
Explicit opt-out for users without Quick Unlock who don't want to retype the
master password after every sleep. Default ON = exact historical behavior
(lock on WTS lock/suspend, with the documented Quick Unlock exemption —
DPAPI already gates access via the Windows account). Synced setting,
Settings > Security.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Single choke point: copySecure overrides any positive clearAfterMs with the
clipboardClearSeconds setting (0 = user disabled). The 15 call sites keep
passing 30000 unchanged — positive just means "auto-clear this secret";
explicit 0 (username copies) still never clears. Synced setting + a select in
Settings > Security (Clipboard privacy). Win+V history exclusion is
deliberately NOT exposed — a password manager must not offer to leak into
history / cloud clipboard.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
The Ctrl+A + Del sent before each field misbehaves on targets where Ctrl+A
isn't select-all (terminals, some remote desktops). New synced setting
(autofillClearField, Settings > Autofill) gates it: JS appends clear=0 to
cmd://autofill/execute when off; ExecuteAutofill wraps the three
SendSelectAllAndDelete calls behind AClearFirst. Absent param = ON, so
existing behavior is unchanged.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
app.js 9363 -> 7962 lines. Three new classic-script modules:
- app.attachments.js (250): blob crypto + upload/download UI, pure
declarations, loads before app.js
- app.autofill.js (276): Win32 combos, title->entry matching, picker,
pure declarations, loads before app.js
- app.unlock.js (907): Quick Unlock + PIN + recovery code grouped (same
"enter without master pw" theme); assigns Bridge.onPinResult /
onQuickUnlockResult at top level so it loads AFTER app.js, like app.sync.js
Audit viewer stays in app.js (only 65 lines, not worth a file). Clipboard
bridge helpers stay too (were interleaved in the quick-unlock section but
unrelated). Registered in BuildAssets whitelist + index.html + APP_PARTS.
Verified in-app: quick unlock cold-start, attachment upload/download.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Auto-backup no longer wipes the stored password when disabled, so
re-enabling reuses it silently. A dedicated "Set/Change backup password"
button (mirrors sync) owns the password, with a warning status when
unset. Corrected the stale hint that claimed the backup pwd was derived
from the master password. Added icons to each settings tab.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Extracted the autofill toggle + hotkey combos out of the Security section into
their own .slideover-field labelled "Autofill", added an Autofill tab + map
entry. 5 tabs now.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Wrapped the tab bar + body in a .settings-main flex row; tabs now stack
vertically on the left with a right border, body scrolls on the right. JS
unchanged (toggles .is-tab-hidden on sections regardless of layout).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Long settings panel → 4 tabs (Général / Sécurité / Account / Sync & Backup).
Each section (.slideover-field) is keyed by its label text to a tab via
SETTINGS_TAB_OF; applySettingsTab toggles .is-tab-hidden on the rest. No HTML
restructure (sections were already .slideover-field siblings), no dep. The
existing settings search composes: a live query suspends the tab filter so
cross-tab matches show, clearing it restores the active tab.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Eighth slice. Quick search wasn't contiguous — its functions sat on both
sides of the cheatsheet and password-history modals (lines 725-1104). Rather
than a fiddly non-contiguous cut, the whole overlay cluster is extracted as
one byte-identical block: js/app.overlays.js (quick search + cheatsheet +
password history). Pure declarations, no top-level side effects → loads
before app.js; all state/api/Bridge/render/decryptPwd refs resolve via shared
global scope at call time.
- Byte-for-byte identical; syntax OK on all nine app parts; 62/62 tests green.
- index.html + BuildAssets whitelist + harness APP_PARTS updated.
app.js: 11936 → 9170 lines (8 modules extracted, ~2770 lines). Load order:
argon2 → crypto → totp → favicon → import → backup → health → overlays →
app → sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Seventh slice of the app.js split. Moves the vault-health dashboard
(computeHealthCache, healthScoreBand, renderHealthDashboard/Section,
openEntryForFix, entryAgeDays + scoring consts) to js/app.health.js. Pure
declarations, no top-level side effects → loads before app.js. Uses
computeStrength/decryptPwd/state/api via shared global scope at call time.
- Byte-for-byte identical extraction; syntax OK on all eight app parts.
- auditCache/auditFilter sit in this var block but drive the separate
Audit-log viewer in app.js — they ride along and resolve cross-file via
shared scope (documented).
- index.html + BuildAssets whitelist + harness APP_PARTS updated.
- 62/62 tests green.
app.js: 11936 → 9545 lines (7 modules extracted).
NOTE: quick search is NOT contiguous (interleaved with cheatsheet +
history-modal code, lines 886-1063), so a clean byte-identical extraction
isn't trivial — deferred.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Per user request, swap the quick-search click/key mapping so the common case
(fill just the password) is the plain left-click / Enter:
left click / Enter → password only (was: full user+Tab+pwd)
right click / Shift+Enter → full user+pwd (was: username only)
Ctrl+click / Ctrl+Enter → username only (was: password only)
Keyboard mirrors the mouse. Copy-mode (tray/palette, no HWND target) shares
the same `mode`, so it shifts too: click/Enter copies password, Ctrl+click/
Ctrl+Enter copies username (right-click's 'full' has no copy meaning → pwd).
Updated the dynamic hint, the static index.html hint, and CLAUDE.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sixth slice of the app.js split. Moves the favicon fetch/cache section
(faviconHost, saveEntryIcon, ensureEntryFavicon, backfillFavicons,
clearAllFavicons) to js/app.favicon.js. Pure declarations, no top-level
side effects → loads before app.js.
- Code moved byte-for-byte; no duplicate const; syntax OK on all 7 app parts.
- NEW: js/tests/favicon.test.js — 7 tests for faviconHost, the pure
URL→validated-hostname function that decides which domain is sent to the
DuckDuckGo proxy (a bug there leaks the wrong host). Covers scheme/www/
path/port stripping, non-hostname rejection, malformed dotting, unsafe
chars, and the 253-char DNS cap.
- Fixed an inaccurate source comment surfaced by the tests: it claimed raw
IPs "stay valid", but the TLD rule /\.[a-z]{2,}$/ rejects a numeric final
label, so IPs get no favicon lookup (fine). Test pins the real behaviour.
- Suite: 55 → 62 tests, all green. Assets regenerated (8 ordered JS files).
app.js: 11936 → 9790 lines (6 modules extracted).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fifth slice of the app.js split. Moves the scheduled encrypted-backup
feature (config, retention, runAutoBackupNow/runAutoBackupIfDue) to
js/app.backup.js. Pure declarations + two consts, no top-level side effects
→ loads before app.js; uses encryptExportPayload (app.import.js), Bridge,
api, state via shared global scope at call time.
- Byte-for-byte identical extraction; no duplicate const; no top-level
backup reference left in app.js; syntax OK on all six app parts.
- index.html + BuildAssets whitelist + harness APP_PARTS updated; assets
regenerated (manifest embeds all 7 ordered JS files).
- 55/55 tests green.
app.js: 11936 → 9900 lines — now under 10k. Five modules extracted
(~2000 lines): argon2 → crypto → totp → import → backup → app → sync.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Fourth slice of the app.js split. Moves TOTP (base32Decode, generateTOTP,
parseOtpAuthUri) plus the TOTP-secret and custom-field AES-GCM wrappers to
js/app.totp.js. Loads before app.js (pure declarations), after app.crypto.js
(uses encryptPwd/decryptPwd). Also called by app.import.js and app.sync.js
via shared global scope.
- Byte-for-byte identical extraction; no duplicate const; syntax OK on all
five app parts.
- NEW: js/tests/totp.test.js — 13 tests including the 5 RFC 6238 Appendix B
reference vectors (generateTOTP reads Date.now(), so each case stubs the
sandbox clock to the vector's fixed time), base32 decode edge cases, and
parseOtpAuthUri. Extraction AND new coverage in one slice.
- Suite: 42 → 55 tests, all green.
- Assets regenerated (manifest now embeds all 6 ordered JS files:
argon2 → crypto → totp → import → app → sync); also fixes the previous
import commit's not-yet-rebuilt manifest.
- Delphi build artifacts (*.vrc, *.$manifest) gitignored.
app.js: 11936 → 10138 lines (4 modules extracted, ~1800 lines).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Third slice of the app.js split. Moves the encrypted export container,
CSV/JSON import parsing (parseCSV, findColumn, parseEntriesFromCSV,
parseEntriesFromJSON), and the doImport/doExport/doExportCSV flows to
js/app.import.js. encryptImportEntry moves here too (also called by
app.sync.js — resolved via shared global scope at call time).
- Byte-for-byte identical to the extracted block; no duplicate const;
no top-level import ref left in app.js.
- Load order: BEFORE app.js (pure declarations, no top-level side effects),
alongside app.crypto.js. Full order: argon2 → crypto → import → app → sync.
- index.html + BuildAssets whitelist + harness APP_PARTS updated.
- Safety net: the 14 CSV tests exercise parseCSV/parseEntriesFromCSV from
the extracted file and stay green (42/42).
app.js: 11936 → 10253 lines (crypto + sync + import now separate, ~1700
lines moved into 3 modules).
NOTE: assets.res not regenerated here (needs brcc32/Delphi) — run
BuildAssets before the next Delphi build to embed js/app.import.js.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Second slice of the app.js split (after crypto). Moves the WebDAV sync
section to js/app.sync.js: transport (_webdavCall), buildSyncSnapshot,
applyRemoteSnapshot (merge + tombstone arbitration), runSyncNow, and the
sync settings UI.
- Byte-for-byte identical to the extracted block (verified before removal);
no duplicate const; no top-level sync reference left in app.js.
- Load order: AFTER app.js (unlike crypto, which loads before) because this
module has a top-level side effect — `Bridge.onWebdavResult = …` — that
needs Bridge/state/api already declared. Rule documented in CLAUDE.md.
- index.html + BuildAssets whitelist + harness APP_PARTS updated; assets
rebuilt to embed the new file.
- Safety net: the existing merge tests exercise applyRemoteSnapshot /
buildSyncSnapshot from the extracted file and stay green (42/42).
app.js: 11936 → 11256 lines (crypto + sync now separate).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
First slice of the app.js split. Approach: ordered classic-script files
loaded via separate <script> tags (argon2.js → app.crypto.js → app.js),
NOT ES modules / a bundler. Classic scripts share one global lexical
environment, so consts/functions cross-reference across files exactly as
in the monofile — zero call-site rewrites, near-zero risk. Chosen over the
audit's esbuild/ES-module suggestion because the code is written entirely
in global scope (functions call each other by bare name everywhere).
- js/app.crypto.js: KDF (PBKDF2 + Argon2id), verifier, AES-GCM encrypt/
decrypt, key persist/restore. Verified byte-for-byte identical to the
original block before removal; no duplicate const across the two scripts.
- index.html + BuildAssets whitelist + test harness updated for the load
order. Harness CONCATENATES app.crypto.js + app.js (node:vm doesn't share
top-level const across separate runInContext calls the way browsers share
it across <script> tags); argon2.js stays a separate IIFE.
- Runtime-validated: rebuilt exe unlocks via quick-unlock and loads/decrypts
entries — the extracted crypto (restoreCryptoKey, verifierFromKeyHex,
decryptPwd) works from the separate file. 42/42 tests green.
- Docs: CLAUDE.md "Découpage frontend" (pattern + rules), file map, tests
README, CODE_AUDIT §3.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 1 of CODE_AUDIT §1.2 — additive, no live account uses Argon2id yet.
- Vendor @noble/hashes@2.2.0 argon2id as js/argon2.js (esbuild IIFE exposing
globalThis.NobleArgon2). Pure-JS, not WASM: CSP is script-src 'self' with no
wasm-unsafe-eval, so WASM would require weakening it. Verified against the
RFC 9106 §5.3 test vector. Server needs zero Argon2 (zero-knowledge: it only
ever SHA256-wraps the client verifier).
- app.js: deriveKeyBytes(pwd, salt, algo, iters, argonParams) branches Argon2id
vs PBKDF2; deriveKeyAndVerifier refactored around it. New markers
HASH_ALGO_ARGON2='argon2id-v2' + ARGON2_DEFAULT_PARAMS (OWASP m=19MiB,t=2,p=1,
~0.65s/unlock). isDecoupledVerifierAlgo() generalises the decoupled-verifier
rule to any '-v2' scheme so argon2id-v2 inherits it. AES key is still ALWAYS
the raw KDF output → entries decryptable, legacy accounts untouched.
- index.html loads js/argon2.js before app.js; added to BuildAssets whitelist;
test harness loads it into the sandbox first.
- Tests: +5 (RFC 9106 vector via vendored bundle, argon2 branch derives Argon2
key not PBKDF2, decoupled verifier, AES round-trip under Argon2 key). 40/40.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Large file save (fixes black screen on big attachment download / export)
- Bridge.saveFile streams anything over ~1MB base64 in chunks through the
cmd:// channel instead of stuffing the whole payload in one URL — a
multi-MB base64 URL blew past WebView2's ~2MB navigation cap and blanked
the document (black screen). Small payloads keep the single-shot path.
- Chunks are sent sequentially (each acked via Bridge.onFileChunkAck
before the next) so repeated location.href assignments don't coalesce.
- Delphi accumulates chunks per reqId in a TStringBuilder
(FFileSaveChunks), commits on file/save-commit, and shares the
decode+dialog+write logic with the single-shot path via SaveDecodedFile.
- Chunk size 1MB → far fewer round-trips (a 20MB export dropped from ~67
to ~27 hops).
Busy overlay + progress
- Global spinner overlay (showBusy/updateBusy/hideBusy). doExport shows it
immediately on click — BEFORE the entry-decrypt + attachment-fetch loop
that is the real cost — with a 0ms yield so it paints before the thread
blocks (was appearing 3-5s late). Phases: "Reading vault… N/total" →
"Encrypting export…" → "Preparing file… N%" (real chunk progress).
Attachment download shows the same for files > 512KB.
- Spinner ring used an undefined --bg-elev-3 (invalid border → invisible);
switched to --border. Fixed a second stale --bg-elev-3 use on the
settings-search clear button hover.
Password reveal
- promptDialog gets an eye toggle in password mode, so every encrypted
prompt (export, import, backup password, recovery code, sync password)
can show/hide the typed value.
Avatar
- Top-right chip avatar enlarged 22px → 30px with the chip padding
rebalanced.
Rebuild: BuildAssets + F9 (UMainForm.pas changed for the chunk handlers).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Profile picture / avatar
- users.avatar_b64 column (nullable, cosmetic, not encrypted) + GET/POST
/avatar endpoints mirroring the settings handler pattern.
- Top-right chip + Settings→Account show a round avatar: custom picture
if set, otherwise the username's initial on a deterministic
hash-picked colour (stable across renders).
- Upload downscales + center-crops to a 128px JPEG via FileReader →
data: URI (NOT blob:, which the CSP's `img-src 'self' data:` blocks)
before POSTing. Remove button clears it.
- Carried in the encrypted JSON export; restored on import only when the
current account has no picture (never clobbers a local one).
Tombstone restore-then-sync fix
- POST /entries and POST /entries/bulk-import now DELETE any tombstone
matching an inserted uuid (same transaction) so a restored backup
isn't re-killed on the next sync by its own stale tombstone.
- applyRemoteSnapshot arbitrates remote tombstones by timestamp: a
tombstone is skipped when the local entry with that uuid is newer than
deleted_at (resurrection wins). Ties / unparseable timestamps favour
KEEP. loadEntries() up front so updated_at reflects the live rows.
WebView2 navigation race
- Black-window-on-cold-start fix: the 1.5s nav timer no longer consumes
FPendingURL when WebView2 isn't initialised yet (it re-arms, bounded
to ~10 retries). FBrowserInitialized flag set in OnInitialized; after
the retry budget we Navigate best-effort rather than loop forever.
Sync UX
- Bidirectional toast: "pulled X new · Y updated · Z deleted · pushed N
entries" so a 0/0/0 pull still shows the vault was uploaded.
- FolderPOST/PUT: pre-declare ftString on color/icon params (fixes the
earlier [SQLite]-335 on NULL bind, already in play for CSV import).
Docs
- CLAUDE.md sync section documents tombstone purge-on-insert +
resurrection arbitration.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Sync engine
- Cross-account guard: refuse to merge a remote snapshot whose
username differs from the currently-signed-in one (confirm dialog,
Cancel by default) so a shared WebDAV URL / same sync password
between accounts stops silently mixing vaults.
- Local tombstones veto: skip any remote entry whose uuid is already
in the local entry_tombstones table — otherwise a perm-delete on
this device was getting undone on the next pull.
- "deleted" counter fixed: report only tombstones that actually
removed a live local entry this round, not the accumulated history
the toast used to inflate ("73 deleted" for 48 real deletes).
- Push failure surfaces syncStatus reset + toast so state doesn't
get stuck on a stale phase label.
Sync progress feedback
- Inline #syncStatus label next to the Sync button reports each
phase: Local backup… → Pulling… → Merging… → Preparing… N/total
(per-entry counter during the slow buildSyncSnapshot decrypt loop)
→ Encrypting… → Pushing…, then clears.
- Sync now / Test connection buttons are disabled while any run is
in flight so double-clicks can't kick off a concurrent sync.
Import (JSON only — CSV out of scope)
- Uuid-aware dedup: split parsed rows into fresh (new uuid) vs
overlaps (uuid already present locally).
- Overlaps prompt: confirm dialog offers Overwrite (restore/roll
back) or Skip. Overwrite PUTs the file's payload over each match;
Skip drops them and only imports fresh. Prevents the "re-import
doubles everything" regression while still allowing restore.
- Bulk-import call is skipped entirely when there's nothing fresh to
send (avoids a POST with an empty entries array).
- Template notes with empty body but populated custom_fields
(credit-card, ssh-key, etc.) no longer skipped as "note body
required" — kept as long as at least one custom field has a value.
Sidebar / uncategorised view
- "(no folder)" pseudo-entry stays visible whenever the vault has
any real folder, so it always works as a drag target for
uncategorising — and doesn't vanish mid-action when the user is
currently viewing it.
- Header title reads "(no folder)" for state.view === 'folder:All'
instead of the ambiguous "All".
- Dedicated empty-state copy for the uncategorised view.
Rebuild assets required.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>