Commit Graph

20 Commits

Author SHA1 Message Date
r-zakarya a2ac0e393b fix(sync): l'URL enregistree est convertie aussi a la lecture des reglages
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 16:00:10 +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 3c16b41714 feat(sync): GitHub en plus de Gitea, et refus d'un depot public
GitHub parle la meme API des contenus (base64, sha, corps du PUT) : meme
chemin, reconnu a api.github.com/repos/. User-Agent pose partout (GitHub
refuse sans). Un depot public est refuse avant toute lecture ou ecriture,
Gitea comme GitHub : l'historique garde tous les instantanes chiffres.
Telephone : github.com/O/R se convertit en URL d'API et inversement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-18 10:36:18 +01:00
r-zakarya 780eecc11b feat(sync): le QR porte un lien pmserver://sync ouvert directement par l'app
Le scanner du telephone envoyait le texte a une recherche web (Google,
Chrome) : le jeton serait parti a un appui pres. Le lien a schema propre
n'a qu'une application pour l'ouvrir. Rien n'est enregistre avant Save.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 18:29:37 +01:00
r-zakarya 9bee179d1b fix(sync): QR a sa vraie taille, la regle svg des icones l'ecrasait
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 18:22:33 +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 fba05f1ce5 fix(import): la restauration dupliquait l'historique, comme la sync
Le meme defaut, par une autre porte : restaurer une sauvegarde par-dessus
des entrees existantes passe aussi par encryptImportEntry puis PUT, donc
re-chiffre un mot de passe inchange et fait croire au serveur qu'il a
change. La sync etait la cause de ce qui se voyait, pas la seule.

La regle est extraite en une fonction, keepCiphertextIfSamePassword,
appelee par les deux. Les re-publications partielles - tag, dossier,
migration des metadonnees - sont hors de cause : elles reutilisent deja
le chiffre stocke.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 23:47:36 +01:00
r-zakarya 8905599214 fix(sync): la synchronisation dupliquait l'historique des mots de passe
L'instantane transporte le mot de passe EN CLAIR, et encryptImportEntry
le re-chiffre avec un IV neuf : le chiffre differe donc toujours, meme
quand le mot de passe n'a pas bouge. Or le serveur decide d'ecrire une
version dans l'historique en comparant les chiffres
(`encrypted_password <> :newenc`) - il voyait une difference la ou il n'y
en avait aucune, et chaque synchronisation qui redescendait une entree y
deposait un doublon date du jour. L'historique montrait la meme valeur
deux fois, a des jours d'ecart.

Le serveur ne peut pas trancher, il ne voit que du chiffre. Seul le
client connait les deux clairs : il compare, et rend le chiffre d'origine
quand ils sont egaux - ce que le commentaire du handler appelle deja
"JS reuses originalEncrypted bit-for-bit".

Trouve en deroulant la serie H sur le telephone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-28 23:33:21 +01:00
r-zakarya ba4ab18d0f feat: the last three passwords travel, and the phone can show them
The desktop keeps twenty previous versions per entry and never sent any. It
now sends three.

Three, not twenty, because the trade is not symmetrical. On the desktop they
sit encrypted column by column and never move. In the snapshot everything
travels in the clear inside the container, and comes down into a phone cache
sealed only by the device key - not by the master password. An old password
stays sensitive: it is often still valid somewhere else. The real use of the
list is short - "I changed it recently, what was it?" - and three cover it.
The other seventeen would be copies with no need facing them.

The phone reads them ON DEMAND from the raw snapshot, never into TVault:
loading them with the vault would put every entry's old passwords in the clear
in memory from the moment it opens, for something one looks at now and then.

Copying goes by RANK, not by value. The page displays the text but never sends
it back - that is the bridge's rule everywhere else, and an old password has
no reason to be the exception just because it happens to be on screen.

They leave with the plaintext at lock, on both sides.
2026-08-22 16:14:04 +01:00
r-zakarya f97e44768a fix(desktop): stop pushing a snapshot that changed nothing
Measured on the phone, opening a cached vault: 121 ms to read the sealed file,
14 ms to display it - and then 5718 ms decrypting a snapshot it already had.

The cause is upstream. A sync pushes unconditionally at the end, and the push
is never a no-op: the container is re-encrypted with a fresh salt and IV, so an
identical vault produces completely different bytes. On Gitea that is one more
commit and a new blob sha, so every other device sees a "new" snapshot and pays
a full key derivation to arrive back where it started. It also grew the
repository on every sync that changed nothing, which is the history cost the
plan warns about.

The merged state is now compared against what was pulled, as plaintext -
the only place equality is observable - ignoring snapshot_at, which changes by
construction. Equal means the push is skipped and the sync still counts as done,
timestamp included: having nothing to write is a success, not an absence.

The comparison is deliberately strict. A false negative costs one commit; a
false positive would silence a push that carried a change and lose it for the
other devices. Four tests fix where that line sits, including a modified
updated_at (which arbitrates the merge elsewhere) and a tombstone (a deletion
that has to propagate).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 15:41:44 +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 c3c6bce406 fix(sync): a trashed entry is no longer resurrected as a duplicate
The trash is a soft delete: no tombstone is written and the rows are absent from state.entries, so the merge read their uuids as missing locally and POSTed them back as fresh live entries. Trashing 30 entries then syncing produced 30 live duplicates alongside the trashed originals - two rows sharing one uuid.

applyRemoteSnapshot now loads the trash and skips any uuid found there. Emptying the trash is what writes the tombstone that propagates the delete.
2026-08-07 12:38:07 +01:00
r-zakarya 5ce18e7ab5 fix(prefs): stop pulling secret prefs into the renderer just to render the UI
A renderer dump taken after a quick-unlock, with nothing opened, contained the auto-backup password in cleartext - inside an ExecuteJavaScript string and in the DevTools protocol buffers - and it survived the auto-lock that wipes the vault key.

New prefs/has answers set-or-not without moving the value. The settings panel uses that; only the paths that encrypt a backup, test the WebDAV connection or run a sync fetch the secrets themselves.
2026-08-02 23:21:03 +01:00
r-zakarya 30e37b3fbd fix(import): restore favorite + pinned on import/backup restore
A pinned entry came back unpinned after export→import. Wider than pinned:
`favorite` was WRITTEN into the export/backup/sync containers but nothing
ever read it back — not parseEntriesFromJSON, not encryptImportEntry, not
the server INSERTs. So it was a write-only field, and a restore silently
dropped both flags. `pinned` wasn't in any container at all.

Wired the full round-trip:
- containers: export + auto-backup now carry `pinned` (favorite was there).
- parseEntriesFromJSON reads both back (absent → 0, e.g. foreign CSV).
- encryptImportEntry passes them through to the body.
- server create + bulk-import INSERTs write favorite/pinned
  (GetValue<Boolean>(..., False) → 0 default). PUT still ignores them, so a
  sync update can't clobber local flags.

Sync stays device-local as decided 2026-07-11: buildSyncSnapshot now OMITS
favorite explicitly (it was dead weight before; with the passthrough live,
leaving it in would have started propagating it). Commented so the intent
isn't re-broken.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 16:06:05 +01:00
r-zakarya a6ba0100ee feat(icons): propagate icon_custom through export / import / sync
Follow-up to 18fe13a: the custom-icon flag now survives round-trips instead
of resetting to fetched on a restore.

- Containers write icon_custom: manual export (doExport), auto-backup, sync
  snapshot; parseEntriesFromJSON carries it back (default 0 when absent, e.g.
  foreign CSV).
- encryptImportEntry ships it in the body (shared by bulk-import + sync
  create/update).
- Server create + bulk-import INSERTs read icon_custom
  (GetValue<Boolean>('icon_custom', False) → 0 default). An empty icon always
  forces icon_custom=0.

Now a custom icon stays custom after export→import and across sync — it won't
be silently reclassified as cache and clobbered by Re-fetch-all on the other
device.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-25 15:21:04 +01:00
r-zakarya 92ed153bc0 fix(sync): surface attachment/folder restore failures (audit 2.3)
applyRemoteSnapshot swallowed attachment + folder restore errors in silent
catch blocks. Count them (attFailed/folderFailed) and warn in a toast after
the sync summary. These don't abort the push (the entry synced, only its
attachment/folder didn't) unlike a failed entry import.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 18:32:15 +01:00
r-zakarya 9bf1e4f571 fix(sync): warn when pre-sync backup has no folder (was silent no-op) 2026-07-10 12:29:07 +01:00
r-zakarya c559e75310 feat(sync): enforce strong sync password (§1.4/§4)
The sync password is the only thing protecting the remote snapshot, but it
accepted 8 chars. Gate raised to 12+ chars AND computeStrength >= 50 (reused
from app.js — no zxcvbn dependency).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 21:52:51 +01:00
r-zakarya c58424d58c feat(sync): include avatar in sync snapshot + auto-backup (multi-device)
The profile picture (users.avatar_b64, cosmetic/unencrypted) travelled only
in the manual export. Now it's also in buildSyncSnapshot and the auto-backup
container, so a new device / a restore picks it up.

- Restore is ADDITIVE (mirrors the import path): applyRemoteSnapshot adopts
  remote.avatar_b64 only when the local device has no avatar — never clobbers
  a locally-set picture. No per-avatar timestamp to arbitrate, so changing an
  existing avatar doesn't propagate (cosmetic, accepted).
- +2 merge tests (adopt-when-empty, don't-clobber-when-set). 65/65 green.
- Server /avatar endpoint unchanged (already accepts {avatar_b64}).

Closes the avatar item of CODE_AUDIT §4.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 20:06:31 +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