Commit Graph

77 Commits

Author SHA1 Message Date
r-zakarya bbd4e2ef06 feat(notes): notes de commandes, une commande copiee ou tapee a la fois
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>
2026-10-10 18:24:49 +01:00
r-zakarya 63e532d5bb feat(entries): Precedent / Suivant parmi les entrees ouvertes
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>
2026-10-10 17:39:24 +01:00
r-zakarya 28f8a42846 feat(hotkeys): chaque raccourci se desactive (Suppr pendant la capture)
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>
2026-10-05 18:15:29 +01:00
r-zakarya 28349c10c8 fix(quick-search): Esc ne sort plus du cadre, et la note dit qu'elle se copie
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>
2026-09-28 12:12:06 +01:00
r-zakarya 7c26716831 feat(quick-search): interrupteur Copy / Fill quand une fenetre cible existe
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>
2026-09-28 12:07:57 +01:00
r-zakarya 652815ea98 feat(quick-search): boutons par entree - utilisateur, mot de passe ou note, code 2FA (s'ils existent)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 23:15:49 +01:00
r-zakarya 72f9acd733 feat(totp): scanner le QR 2FA affiche a l'ecran (capture + ZXing), seul un otpauth:// part vers la page
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:24:35 +01:00
r-zakarya 508d5345bf feat(fillbar): option fond transparent (cle de couleur), l'opacite reste sur les boutons
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 22:12:37 +01:00
r-zakarya df2d0f55b9 docs(sync): libelle 'Git repository (Gitea or GitHub)' au lieu de Gitea seul
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 16:16:03 +01:00
r-zakarya 329b0e3ce5 feat(sync): le PC accepte l'adresse du depot (github.com/O/R.git, Gitea) et la convertit en URL d'API
Garde : un dernier segment qui ressemble a un fichier (.json) reste une URL
WebDAV.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 15:40:45 +01:00
r-zakarya ba06d4584e feat(fillbar): bouton Settings sur la barrette (icone roue ou texte), reglable comme les autres
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 11:14:41 +01:00
r-zakarya d7a8b4ddff feat(fillbar): onglet Fill bar dans les reglages, icones ou texte, boutons Add et Search
- boutons choisis (Full, Password, 2FA, Add current window, Search, Open),
  style icones (Segoe MDL2 Assets) ou texte, opacite au repos, allumee ou non
- clic droit sur la barrette : Show icons / Show text en plus de l'opacite
- info-bulles sur chaque bouton
- Add et Search reprennent les corps extraits de Ctrl+Shift+A et Ctrl+Shift+Q

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 18:03:00 +01:00
r-zakarya d8f4f19041 fix(licence): Register ne clignote plus avant d'etre masque
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>
2026-09-17 06:08:37 +01:00
r-zakarya 253e81252c docs(licence): scenario d'essai, et le bouton qui le rend jouable
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>
2026-09-05 18:06:45 +01:00
r-zakarya 8f0bbb580f feat(licence): Settings affiche le porteur de la licence
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>
2026-09-05 16:19:24 +01:00
r-zakarya 405ac7fac4 feat(licence): lier la licence au poste (carte mere + disque)
C'etait la decision prise, et je l'avais mal lue : "pour le moment 1"
repondait a la question 1, pas "une seule licence".

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

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

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

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

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 15:16:35 +01:00
r-zakarya 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 23dc0a78ac feat(sync): Sync now dans le menu Tools
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>
2026-08-28 23:59:52 +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 7c86f35fce feat(autofill): Ctrl+Shift+T types the 2FA code
Same workflow as the password-only hotkey: click the 2FA field, press the
combo, the current 6-digit code is typed in.

Reuses the whole autofill pipeline — no new native typing path. The code is
sent in the password slot with an empty username, so Delphi's existing
password-only branch types just the code.

- Bridge: akTotp kind, hotkey id 47, registered/torn down alongside the other
  two autofill combos; an unconfigured combo (vk=0) doesn't count as a
  registration failure.
- Matching filters on totp_secret BEFORE scoring, so the multi-match picker
  never offers an entry that has no code.
- Configurable + synced like the others (autofillHotkeyTotp), collision-checked
  against the other four slots, covered by Reset to defaults and the cheatsheet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 16:53:00 +01:00
r-zakarya 0434eec85e refactor: remove the dead legacy entry modal
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>
2026-07-25 14:35:39 +01:00
r-zakarya 4dfd908a9a style(table): move the column picker into the table header corner
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>
2026-07-21 20:00:56 +01:00
r-zakarya 0d40328a24 feat(hotkey): make the new-entry hotkey configurable + fix tray title capture
Part A — Ctrl+Shift+A (new entry from window title) is now editable like the
other 3 global hotkeys, following the same pattern end to end: state.newEntryHotkey
(synced), a capture button in Settings > Autofill, dup cross-check, reset-to-
defaults, autofillPushHotkeys, and Bridge.setAutofillHotkeys carrying ne_mods/
ne_vk. Delphi gets SetNewEntryHotkey + stores the live combo (FNewEntryHotkeyMods/
Vk) so ExecuteAutofill's post-fill re-register uses the user's combo instead of
resetting to Ctrl+Shift+A. UMainForm parses ne_* unconditionally (works with
autofill off).

Part B — new-entry from the tray pre-filled the title with "Password Manager":
GetForegroundWindow resolved to our own hidden window. Only capture the title
when the foreground window belongs to another process; otherwise pass '' (blank).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 19:49:48 +01:00
r-zakarya a090c64081 feat(security): "Lock vault when Windows locks or sleeps" toggle (default ON)
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>
2026-07-13 05:17:16 +01:00
r-zakarya 8cfa1b0d6a feat(clipboard): user-configurable auto-clear delay (Never/15/30/60/120s)
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>
2026-07-12 11:48:08 +01:00
r-zakarya 72dcc3dd82 fix(autofill): kill residual hotkey chord; dedicated fail-balloon setting
Residual new-entry trigger (1 in 6): password chars go out as
KEYEVENTF_UNICODE (VK_PACKET, can't match a hotkey) — the real chord risk is
the Ctrl+A clear-field, which sends a real VK_A. If the user re-presses
Ctrl+Shift mid-sequence, that VK_A becomes physical Ctrl+Shift+A = our own
new-entry hotkey. ForceReleaseModifiers now runs inside
SendSelectAllAndDelete, at the risky instant, not just once up front.

Balloon: was gated by "Show tray notifications" (OFF for this user) — now
gated by its own synced setting "Tray alert when autofill is blocked"
(autofillFailBalloon, Settings > Autofill, default ON), carried as notify=0
on cmd://autofill/execute. ShowBalloon no longer gates internally; each
caller applies its own setting.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-12 05:23:02 +01:00
r-zakarya b869effee5 feat(autofill): "Clear the field before typing" setting (default ON)
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>
2026-07-12 04:52:35 +01:00
r-zakarya d583a3f7d3 refactor(js): extract attachments, autofill, unlock modules from app.js (3.1)
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>
2026-07-11 19:28:19 +01:00
r-zakarya a42e4b205d feat(entries): encrypt template at rest, guided tour, import fixes, cleanup
Batched session work sharing app.js / index.html / Entries.pas, so it can't
split cleanly without interactive hunk staging.

- feat: encrypt `template` metadata at rest (template_enc/iv, added to
  ENCRYPTED_META_FIELDS). withEncryptedMeta skips an absent template key so
  partial re-ships (add-tag, move-to-folder) don't wipe it via LHasTemplate.
  Cleartext column kept as migration fallback. +3 unit tests.
- feat: first-run guided tour ("How it works") — spotlight + bubble, no GIFs,
  re-launchable from Settings, seen-flag in DPAPI prefs.
- fix(import): preserve original created_at on restore (was stamped to import
  time); restore entry icons on overwrite (PUT ignores icon_b64).
- fix(settings): correct clipboard-privacy copy (already excluded from Win+V);
  PIN text 4-6 -> 4-12; reorder Set-PIN above unlock-method; move tray/startup
  toggles to General; dedicated backup-password button + warning status; tab icons.
- chore: remove dead legacy monolith (app-legacy.js, index-legacy.html,
  style-legacy.css) + unused passkeyBtn stub.
- docs: full-source review (CODE_AUDIT 6b), template + favorite/pinned notes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 18:32:37 +01:00
r-zakarya 45ba47f772 feat(settings): keep backup pwd on disable, dedicated pwd button, tab icons
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>
2026-07-10 16:53:45 +01:00
r-zakarya 064ad14156 style(settings): move Test connection under the WebDAV credentials 2026-07-10 12:36:14 +01:00
r-zakarya bfce02ae6b style(settings): move Import next to Export 2026-07-10 12:19:22 +01:00
r-zakarya 5144c2aaf7 style(settings): English tab labels to match the UI (General/Security) 2026-07-10 05:26:45 +01:00
r-zakarya 374a6b3fe9 feat(settings): split Autofill into its own tab
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>
2026-07-10 05:21:57 +01:00
r-zakarya 18533486b7 style(settings): move tabs to a vertical column on the left
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>
2026-07-10 04:56:16 +01:00
r-zakarya bec94930b4 feat(settings): group settings into 4 tabs
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>
2026-07-09 23:42:22 +01:00
r-zakarya 9e424efaf4 refactor(js): extract quick-search overlay cluster from app.js (§3.1)
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>
2026-07-08 18:43:55 +01:00
r-zakarya b44b05118e refactor(js): extract vault-health module from app.js monofile (§3.1)
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>
2026-07-08 15:07:42 +01:00
r-zakarya 8e0e7fd330 feat(quicksearch): remap fill modes — left/Enter=password, right/Shift=full
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>
2026-07-08 14:24:57 +01:00
r-zakarya a7ad81c708 refactor(js): extract favicon module + add faviconHost tests (§3.1)
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>
2026-07-05 16:58:16 +01:00
r-zakarya 4fd768d4cf refactor(js): extract auto-backup module from app.js monofile (§3.1)
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>
2026-07-05 16:46:01 +01:00
r-zakarya 5fc07aed7a refactor(js): extract TOTP module + add RFC 6238 tests (§3.1)
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>
2026-07-05 16:35:29 +01:00
r-zakarya 97a19836a0 refactor(js): extract import/export module from app.js monofile (§3.1)
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>
2026-07-05 16:24:11 +01:00
r-zakarya 2d309f8988 refactor(js): extract sync module from app.js monofile (§3.1)
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>
2026-07-05 16:09:02 +01:00
r-zakarya ca8081987d refactor(js): extract crypto module from app.js monofile (§3.1 start)
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>
2026-07-05 15:44:06 +01:00
r-zakarya 2bd0fcfbf8 feat(crypto): Argon2id KDF foundation (vendored, not yet adopted)
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>
2026-07-05 14:13:36 +01:00
r-zakarya c5fe26ce94 feat: chunked file save + export progress spinner + password-reveal + avatar size
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>
2026-07-03 16:56:47 +01:00
r-zakarya 7440d07793 feat: profile avatar + tombstone-restore fix + WebView2 nav race + sync summary
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>
2026-07-02 23:36:36 +01:00
r-zakarya 0a3372c151 feat: sync guards + import restore mode + inline progress + uncategorised polish
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>
2026-07-01 20:24:35 +01:00