fix(android): the merge lost folders, and the pending dot was on one screen
Two defects found by re-reading before adding more, along with a test plan. MergeSnapshots merged entries and tombstones but not folders. A folder created on the phone - offline, or just before another device published - vanished from the list while the entries kept its name: filed under a folder nothing displays, unfindable without knowing it. That is exactly what EnsureFolder was written against, come back through another door. The pending dot sat on the padlock, which only exists in the list. After locking one lands on the home screen - precisely where one decides to sync - and it said nothing there. It is on Sync as well now. TESTS.md records what the reading checked and what it left alone, with reasons for the two defects deliberately not fixed, and a scenario that says what each step must PRODUCE rather than only what to do.
This commit is contained in:
@@ -203,7 +203,8 @@ end;
|
|||||||
function MergeSnapshots(const ALocalJson, ARemoteJson: string): string;
|
function MergeSnapshots(const ALocalJson, ARemoteJson: string): string;
|
||||||
var
|
var
|
||||||
LLocal, LMerged: TJSONObject;
|
LLocal, LMerged: TJSONObject;
|
||||||
LLocalEntries, LOutEntries, LLocalStones, LOutStones: TJSONArray;
|
LLocalEntries, LOutEntries, LLocalStones, LOutStones,
|
||||||
|
LLocalFolders: TJSONArray;
|
||||||
LEntry, LOther: TJSONObject;
|
LEntry, LOther: TJSONObject;
|
||||||
I, J: Integer;
|
I, J: Integer;
|
||||||
LUuid, LMine, LTheirs, LKilled: string;
|
LUuid, LMine, LTheirs, LKilled: string;
|
||||||
@@ -223,6 +224,22 @@ begin
|
|||||||
LMerged.AddPair('tombstones', LOutStones);
|
LMerged.AddPair('tombstones', LOutStones);
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
// 0. LES DOSSIERS DU LOCAL REJOIGNENT CEUX DU DISTANT.
|
||||||
|
//
|
||||||
|
// Oubli trouve a la relecture : la fusion ne portait qu'entrees et
|
||||||
|
// pierres tombales. Un dossier cree ici - hors connexion, ou juste avant
|
||||||
|
// qu'un autre appareil publie - disparaissait donc de `folders`, tandis
|
||||||
|
// que les entrees gardaient son nom. Elles se retrouvaient rangees dans
|
||||||
|
// un dossier que rien n'affiche : introuvables sans le savoir.
|
||||||
|
//
|
||||||
|
// Additif, jamais destructif : on n'enleve pas un dossier du distant
|
||||||
|
// parce qu'il manque ici, c'est aussi la regle du poste.
|
||||||
|
if LLocal.TryGetValue<TJSONArray>('folders', LLocalFolders) then
|
||||||
|
for I := 0 to LLocalFolders.Count - 1 do
|
||||||
|
if LLocalFolders.Items[I] is TJSONObject then
|
||||||
|
EnsureFolder(LMerged,
|
||||||
|
(LLocalFolders.Items[I] as TJSONObject).GetValue<string>('name', ''));
|
||||||
|
|
||||||
// 1. Les pierres tombales locales rejoignent celles du distant. Avant les
|
// 1. Les pierres tombales locales rejoignent celles du distant. Avant les
|
||||||
// entrees : l'etape suivante s'en sert pour arbitrer.
|
// entrees : l'etape suivante s'en sert pour arbitrer.
|
||||||
if LLocal.TryGetValue<TJSONArray>('tombstones', LLocalStones) then
|
if LLocal.TryGetValue<TJSONArray>('tombstones', LLocalStones) then
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
# Client Android — relecture et scénario d'essai
|
||||||
|
|
||||||
|
Relecture du 2026-08-22, avant d'ajouter la vérification de fond, les réglages
|
||||||
|
et les icônes. Le but n'était pas de tout relire mais de chercher là où cette
|
||||||
|
séance a montré que les défauts se logent : **les frontières entre fichiers,
|
||||||
|
les chemins d'erreur, et ce qui traverse les threads.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ce que la relecture a trouvé
|
||||||
|
|
||||||
|
### 1. La fusion ne transportait pas les dossiers — CORRIGÉ
|
||||||
|
|
||||||
|
`MergeSnapshots` fondait `entries` et `tombstones`, pas `folders`. Un dossier
|
||||||
|
créé sur le téléphone — hors connexion, ou juste avant qu'un autre appareil
|
||||||
|
publie — disparaissait donc de la liste, **tandis que les entrées gardaient son
|
||||||
|
nom**. Elles se retrouvaient rangées dans un dossier que rien n'affiche :
|
||||||
|
introuvables sans qu'on le sache.
|
||||||
|
|
||||||
|
C'est exactement la panne contre laquelle `EnsureFolder` avait été écrit, et
|
||||||
|
elle est revenue par une autre porte. Le report est additif — on ne retire pas
|
||||||
|
un dossier du distant parce qu'il manque ici.
|
||||||
|
|
||||||
|
### 2. Le témoin d'attente n'existait que sur la liste — CORRIGÉ
|
||||||
|
|
||||||
|
Le point orange était posé sur le cadenas, qui n'existe que dans la liste. Or
|
||||||
|
après un verrouillage on atterrit sur l'accueil — précisément là où l'on décide
|
||||||
|
de synchroniser. Il est maintenant aussi sur *Sync*.
|
||||||
|
|
||||||
|
### 3. Trois routines ne libèrent pas l'indicateur d'occupation dans un `finally`
|
||||||
|
|
||||||
|
`SaveAttachment`, `RegisterLocal` et `LoginLocal` le font dans le chemin normal
|
||||||
|
**et** dans leur `except`, ce qui couvre tout ce qui descend d'`Exception`. Une
|
||||||
|
erreur d'un autre genre laisserait le voile en place.
|
||||||
|
|
||||||
|
**Non corrigé, et assumé** : le minuteur de 25 s le lève de toute façon, et
|
||||||
|
c'est son rôle. Le signaler ici vaut mieux que d'ajouter trois `finally` qui
|
||||||
|
donneraient l'illusion d'une garantie que seul le minuteur apporte.
|
||||||
|
|
||||||
|
### 4. `ParseJSONValue(...) as TJSONObject` fuit sur une entrée qui n'est pas un objet
|
||||||
|
|
||||||
|
Le transtypage lève avant l'affectation, donc la valeur analysée n'est jamais
|
||||||
|
libérée. Dix-sept occurrences. **Non corrigé** : n'arrive que sur un conteneur
|
||||||
|
corrompu, où l'on a de plus gros ennuis, et le processus est de toute façon en
|
||||||
|
train d'échouer bruyamment. À traiter le jour où l'on touche à ces fonctions.
|
||||||
|
|
||||||
|
### 5. Ce qui a été vérifié et va bien
|
||||||
|
|
||||||
|
- **Échappement HTML** : `esc()` couvre nom, sous-titre, initiale, URI d'icône,
|
||||||
|
libellés, valeurs, noms de dossiers, options de liste. Une entrée nommée
|
||||||
|
`<script>` ne s'exécute pas. C'était le risque réel — le coffre contient du
|
||||||
|
texte arbitraire et la page construit son HTML par `innerHTML`.
|
||||||
|
- **La file de commandes du pont** libère `sending` dans un `finally` : une
|
||||||
|
exception au retrait de l'iframe ne peut plus rendre l'interface muette.
|
||||||
|
- **Les états du `body`** (`typing`, `selecting`, `searching`, `on-list`,
|
||||||
|
`on-detail`, `is-busy`) ne portent le nom d'aucun élément — le piège qui
|
||||||
|
éteignait la page entière ne se répète nulle part.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Scénario d'essai
|
||||||
|
|
||||||
|
À dérouler dans l'ordre. Chaque étape dit **ce qu'il faut voir**, pas seulement
|
||||||
|
ce qu'il faut faire : un test dont on ne sait pas ce qu'il doit produire ne
|
||||||
|
prouve rien.
|
||||||
|
|
||||||
|
### A. Enregistrement hors connexion — le plus important
|
||||||
|
|
||||||
|
1. Coffre ouvert, **couper les données** (mode avion).
|
||||||
|
2. Modifier un mot de passe → message *« saved here, not published yet »*, et
|
||||||
|
**point orange** sur le cadenas.
|
||||||
|
3. Verrouiller → le point doit être **sur le bouton Sync** de l'accueil.
|
||||||
|
4. **Fermer complètement l'application** (glisser depuis les récentes), rouvrir,
|
||||||
|
ouvrir le coffre → le point doit **être encore là**. C'est l'étape qui
|
||||||
|
éprouve la persistance du drapeau ; sans elle, le reste ne prouve rien.
|
||||||
|
5. Rétablir le réseau, *Sync* → *« Offline changes published »*, point éteint.
|
||||||
|
6. Sur le poste, synchroniser → la modification doit y être.
|
||||||
|
|
||||||
|
### B. La fusion, qui est le cœur du sujet
|
||||||
|
|
||||||
|
1. Téléphone **hors ligne**, modifier l'entrée **X**.
|
||||||
|
2. Sur le poste, modifier une **autre** entrée **Y**, synchroniser.
|
||||||
|
3. Téléphone en ligne, *Sync*.
|
||||||
|
4. **Les deux modifications doivent survivre.** Si une seule survit, c'est
|
||||||
|
l'arbitrage sur `updated_at` qu'il faut regarder.
|
||||||
|
5. Variante avec un **dossier créé hors connexion** sur le téléphone : il doit
|
||||||
|
apparaître dans la barre de dossiers après la fusion, et sur le poste après
|
||||||
|
sa synchronisation. C'est le défaut n° 1 ci-dessus.
|
||||||
|
|
||||||
|
### C. Notes multilignes
|
||||||
|
|
||||||
|
1. **+** → *Note* → zone de texte pleine largeur, *Site* et *Username* disparus.
|
||||||
|
2. Taper plusieurs lignes — la touche Entrée doit **insérer un saut**, pas
|
||||||
|
replier le clavier.
|
||||||
|
3. Enregistrer, rouvrir → les ruptures doivent être là **en lecture**.
|
||||||
|
4. Basculer *Login* → *Note* après avoir tapé : la saisie doit **survivre**.
|
||||||
|
5. Contre-épreuve : dans un champ d'une ligne (*Name*, code de déverrouillage),
|
||||||
|
Entrée doit toujours **replier le clavier**.
|
||||||
|
|
||||||
|
### D. Sélection et actions de liste
|
||||||
|
|
||||||
|
1. Appui long → barre contextuelle, compteur.
|
||||||
|
2. Deuxième ligne → compteur à 2.
|
||||||
|
3. **Dupliquer** → deux copies, suffixées *(copy)*.
|
||||||
|
4. **Supprimer** → confirmation en deux temps, puis les lignes disparaissent.
|
||||||
|
5. Touche **retour** pendant une sélection → elle s'annule **sans verrouiller**.
|
||||||
|
6. Sur le poste, synchroniser → les suppressions doivent y être répercutées.
|
||||||
|
|
||||||
|
### E. Pièces jointes
|
||||||
|
|
||||||
|
1. Ligne *Attachment* → flèche de téléchargement → le fichier apparaît dans
|
||||||
|
*Téléchargements* sous son nom, accents et arabe compris.
|
||||||
|
|
||||||
|
### F. Code de déverrouillage
|
||||||
|
|
||||||
|
1. Poser un code, ouvrir le coffre, fermer, rouvrir → il est demandé.
|
||||||
|
2. Code faux → *« Wrong unlock code »*, et **Open vault reste offert** (le
|
||||||
|
coffre n'est pas déclaré illisible).
|
||||||
|
3. Changer le code : ancien dans le premier champ, nouveau dans le second,
|
||||||
|
*Open vault* → *« Unlock code changed »*.
|
||||||
|
4. Contre-épreuve : taper n'importe quoi dans le champ du code puis *Sync* →
|
||||||
|
le code **ne doit pas changer**.
|
||||||
|
|
||||||
|
### G. Empaquetage
|
||||||
|
|
||||||
|
1. `build-apk.ps1 -Release -Keystore ... -Alias ...` → la sortie doit afficher
|
||||||
|
`Signer #1 certificate DN: CN=R Zakarya`. Si elle affiche `CN=Android
|
||||||
|
Debug`, la release ne vaut rien.
|
||||||
|
2. Elle ne s'installe pas d'elle-même, et c'est voulu.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ce qui n'est toujours pas éprouvé
|
||||||
|
|
||||||
|
- **Le chemin `-Release`** du script n'a jamais tourné.
|
||||||
|
- **§1 point 4** — l'invalidation de la clé par enrôlement d'une empreinte,
|
||||||
|
désamorcée par le code de déverrouillage tant qu'un code est posé.
|
||||||
|
- **`MergeSelfCheck`** tourne au démarrage en débogage et écrit
|
||||||
|
`merge selfcheck: the four rules hold` dans le journal. Personne ne l'a
|
||||||
|
encore lu.
|
||||||
@@ -181,8 +181,8 @@
|
|||||||
.btn.ico-btn:active { opacity: .45; }
|
.btn.ico-btn:active { opacity: .45; }
|
||||||
/* Le point des modifications en attente. Sur le cadenas parce qu'il est
|
/* Le point des modifications en attente. Sur le cadenas parce qu'il est
|
||||||
toujours a l'ecran de la liste, et qu'il parle deja de l'etat du coffre. */
|
toujours a l'ecran de la liste, et qu'il parle deja de l'etat du coffre. */
|
||||||
.ico.pending { position: relative; }
|
.ico.pending, .btn.pending { position: relative; }
|
||||||
.ico.pending::after {
|
.ico.pending::after, .btn.pending::after {
|
||||||
content: ''; position: absolute; top: 6px; right: 6px;
|
content: ''; position: absolute; top: 6px; right: 6px;
|
||||||
width: 9px; height: 9px; border-radius: 50%;
|
width: 9px; height: 9px; border-radius: 50%;
|
||||||
background: #ffb44d; box-shadow: 0 0 0 2px var(--bg-top);
|
background: #ffb44d; box-shadow: 0 0 0 2px var(--bg-top);
|
||||||
@@ -1471,7 +1471,11 @@ var PM = {
|
|||||||
/// qu'on ne le cherche pas, mais present a chaque coup d'oeil - c'est la
|
/// qu'on ne le cherche pas, mais present a chaque coup d'oeil - c'est la
|
||||||
/// seule facon de savoir que le depot ignore encore ce qu'on vient de taper.
|
/// seule facon de savoir que le depot ignore encore ce qu'on vient de taper.
|
||||||
setPending: function (on) {
|
setPending: function (on) {
|
||||||
|
// LES DEUX ECRANS, pas seulement la liste. Apres un verrouillage on
|
||||||
|
// atterrit sur l'accueil - precisement la ou l'on decide de synchroniser -
|
||||||
|
// et le cadenas de la liste n'y est plus. Le temoin n'y disait donc rien.
|
||||||
document.getElementById('lock').classList.toggle('pending', !!on);
|
document.getElementById('lock').classList.toggle('pending', !!on);
|
||||||
|
document.getElementById('btnSync').classList.toggle('pending', !!on);
|
||||||
},
|
},
|
||||||
setBusy: function (on) {
|
setBusy: function (on) {
|
||||||
if (on) document.getElementById('busy').firstChild.innerHTML =
|
if (on) document.getElementById('busy').firstChild.innerHTML =
|
||||||
|
|||||||
Reference in New Issue
Block a user