Tous les articles
Release Notes9 min de lecture

Filarr Mobile, semaine du 11 août : cinq correctifs de sync qu'on ne pouvait pas ignorer

Build in public : semaine du 11 août sur l'app mobile Filarr. Cinq bugs de sync réels corrigés sur appareil, quota de stockage enfin branché, 3 326 tests verts. App en finalisation, sortie d'ici un à deux mois.

MB

Mathis Belouar-Pruvot

Avant de shipper l'app mobile, on a un principe : les cas d'erreur doivent être honnêtes. Pas de bannière "je réessaierai" sur un fichier disparu côté serveur. Pas de fichier bloqué en "indéchiffrable" alors que le lecteur vient d'être mis à jour. Pas de quota "non communiquée" quand l'API répond. Cette semaine, on a fermé cinq de ces dettes.

La semaine précédente, on avait posé les grandes briques : graphe de notes, chiffrement AES-256-GCM, 2FA TOTP, sync desktop-mobile, CI/CD Android et iOS. Cette semaine, le rythme a changé. Moins de nouvelles fonctionnalités, plus de travail en profondeur sur des cas réels observés sur appareil physique en test.

C'est la phase que je préfère le moins à écrire et que je trouve la plus importante à traverser. Quand tu construis un produit dont le pitch est "tes fichiers, tes clés, tu peux leur faire confiance", les cas d'erreur ne peuvent pas mentir. 29 commits cette semaine. Voici ceux qui comptent vraiment.


La sync : cinq correctifs réels, constatés sur appareil

La synchronisation zero-knowledge de Filarr est la pièce la plus complexe du mobile. Les fichiers voyagent sous forme de blobs chiffrés opaques : Cloudflare R2 ne voit jamais le contenu. C'est ce qui permet d'être honnête sur la vie privée. Mais ça complique la gestion des erreurs : quand quelque chose ne va pas, il faut distinguer "le réseau est lent", "l'objet n'existe plus sur le serveur", "ce client ne sait pas encore lire ce format" ou "les métadonnées sont périmées". Ce sont quatre réponses différentes, et la mauvaise réponse peut bloquer un fichier définitivement.

L'objet distant qui n'existe plus

Cas observé sur appareil : un fichier avec des métadonnées valides, 1 281 569 octets annoncés, une trace "absence prouvée : true". Le serveur répondait 404. Le cycle de sync en faisait une erreur transitoire réseau et affichait la bannière "transfert interrompu, je réessaierai" à l'infini.

La distinction qui manquait : un 404 sur un objet dont l'absence est prouvée (le serveur a répondu, pas un timeout) n'est pas une panne réseau. C'est un fait stable. J'ai introduit un nouveau motif permanent, missing-remote-object, déclenché uniquement quand l'absence est prouvée et jamais sur un simple doute réseau.

Ce qui est important : le fichier n'est pas supprimé du coffre. On constate, on n'affirme rien. Si un autre appareil possède les octets et les retéléverse, ce motif sera réévalué au prochain cycle. En attendant, le fichier obtient une ligne honnête dans le journal, pas une promesse intenable.

Un point subtil qui m'a pris du temps lors de la revue adverse : le motif "permanent" ne doit jamais dispenser d'essayer si les octets reviennent. C'est la différence entre "ce client ne peut pas faire X" (jugement local, stable) et "le serveur a confirmé que l'objet n'existe pas" (fait externe, potentiellement temporaire). Ici c'est un fait externe : permanent pour la bannière, mais le cycle retente si les conditions changent.

Le verdict d'indisponibilité qui survivait au mauvais lecteur

Deuxième cas réel : quatorze fichiers d'un profil desktop restaient marqués "indéchiffrables" sur le mobile, même après l'ajout du lecteur multipart qui les rendait précisément lisibles. Le cycle rejouait un verdict périmé depuis le cache, sans jamais retenter le téléchargement.

Le rejeu est logique en principe (ne pas repayer un téléchargement voué à échouer). Mais "ce client ne sait pas lire ce format" est un verdict rendu par une version précise du lecteur. Si le lecteur évolue, le verdict doit être reconsidéré.

Solution : READER_CAPABILITY_VERSION. Chaque verdict persisté porte désormais la version du lecteur qui l'a rendu. Une carte d'une version antérieure est réputée périmée et le cycle retente automatiquement. À chaque fois qu'on élargit ce qu'on sait lire (nouveau format de conteneur, nouvelle disposition de transport), on incrémente ce compteur. Ce n'est pas un détail anodin : chaque incrément coûte un téléchargement par entrée concernée sur tous les appareils. On incrémente seulement quand c'est justifié.

Le condensat qui mentait, le tag GCM qui ne mentait pas

Cause réelle d'une bannière "synchronisation interrompue" éternelle, lue dans le journal d'un appareil de test : "Checksum du manifeste != octets téléchargés". Pas un objet manquant, pas une panne réseau. Un condensat périmé dans les métadonnées, héritage d'une migration : l'entrée avait été écrite avec l'empreinte d'un chiffré, l'objet R2 avait ensuite été réécrit sur la même clé déterministe. Le condensat du manifeste et l'objet réel ne correspondaient plus.

L'ancienne logique refusait et renvoyait une erreur définitive à chaque cycle, indéfiniment.

La correction : dégrader vers la preuve forte au lieu du refus. Un condensat de manifeste est une pré-vérification bon marché, écrite par nous. Le tag AES-256-GCM est une preuve cryptographique de provenance et d'intégrité. Si le condensat ne correspond pas, on tente la vérification du tag GCM sur les octets déjà téléchargés (aucun second aller-retour réseau). Si le tag passe sous nos clés candidates, on adopte le fichier et on répare l'entrée dans le manifeste en comparer-et-écrire, pour ne pas écraser une republication concurrente. Si le tag ne passe pas, on rejette comme avant.

Résultat concret : les fichiers bloqués par ce cas précis se débloquent automatiquement au prochain cycle, sans action de l'utilisateur.

Le 404 axios non reconnu comme preuve d'absence

Cas plus technique, mais qui bloquait la purge des fantômes : downloadChunk rend l'erreur axios telle quelle, avec le statut HTTP dans response.status. Le prédicat isProvenAbsent ne lisait que la forme enveloppée. Sur appareil, une méta "fantôme" en 404 perpétuel n'était donc jamais "prouvée absente", la purge ne tirait pas, et la bannière "transfert interrompu" survivait même après les autres correctifs. Le prédicat lit maintenant les deux formes de l'erreur axios, y compris la cause sous-jacente.

Le verrou fantômes / autorité

Le plus tordu de la semaine. Un cercle bloquant constaté sur appareil : la purge des entrées "fantômes" (entrées distantes sans chemin local) exigeait l'autorité du client. L'autorité exigeait un cycle sans "corruption". Et la seule "corruption" restante, c'étaient les fantômes eux-mêmes. Bannière éternelle sur trois faces simultanées : corruption, transfert interrompu, entrée imposable.

Trois volets pour en sortir. D'abord, le motif unplaceable-entry : une entrée distante sans chemin d'accueil est un défaut de schéma, pas une atteinte à l'intégrité. Elle ne déclenche plus la bannière rouge et l'octroi d'autorité redevient atteignable. Ensuite, la migration publie maintenant localPath sur chaque entrée de blob au moment de la publication du manifeste, ce qui empêche la naissance de nouvelles entrées imposables. Enfin, la purge sous autorité du redondant prouvé : une entrée imposable dont le plaintextChecksum a un jumeau posable et porteur d'octets dans le même manifeste est retirée, suppression de l'ancienne clé R2 incluse. Sans jumeau prouvé, rien n'est purgé, pour éviter la purge mutuelle entre deux entrées imposables identiques.


L'accueil enfin honnête

Un bug plus simple, mais visible par tous les utilisateurs avec un compte cloud : l'écran d'accueil affichait "Utilisation non communiquée" en permanence, même sur un compte avec abonnement actif.

Tout existait pourtant : le worker expose storageUsed et storageLimit sur GET /billing/status, le client a getBillingStatus typé, et resolveStorageBlock sait calculer la jauge et son seuil d'alerte. Seul l'appel manquait. HomeScreen appelait resolveStorageBlock({ accountMode, tier }) sans jamais passer les octets du quota. Le libellé disait donc vrai, indéfiniment.

Corrigé. Le quota est lu au montage, au changement de mode de compte, et à chaque cycle complet. Best-effort et jamais bloquant : hors ligne ou serveur muet, le bloc retombe honnêtement sur "non communiquée". Un chiffre inventé ou vieux de trois jours présenté comme courant serait pire qu'aucun chiffre du tout. Aucun appel en mode compte local, où le bloc n'est pas rendu.

Deuxième problème mesuré en parallèle : sept appels à /billing/status en vingt secondes, parce que l'accueil se remonte à chaque retour d'onglet. Résolu avec une fenêtre de fraîcheur de deux minutes. Assez courte pour qu'un retour sur l'accueil après un import montre le bon chiffre, assez longue pour que la navigation ordinaire ne consomme rien en réseau. L'horodatage n'est posé que sur un succès : une coupure passagère ne condamne pas le bloc à "non communiquée" pendant toute la fenêtre.


Ce que ça change concrètement

Si tu utilises l'app mobile en test, voici ce qui est différent après cette semaine :

  • Les fichiers dont l'objet serveur a disparu n'affichent plus de bannière de retry infinie. Ils obtiennent une ligne honnête dans le journal du coffre.
  • Les fichiers bloqués en "indéchiffrable" par une ancienne version du lecteur sont re-testés automatiquement au prochain cycle de sync.
  • Les fichiers bloqués par un condensat périmé dans le manifeste se débloquent sans action de ta part.
  • L'écran d'accueil affiche ton quota réel de stockage cloud, mis à jour à chaque cycle.
  • La bannière de corruption ne s'allume plus sur des entrées imposables qui ne sont pas de la corruption.

Rien de visible dans les captures d'écran. Tout de visible dans la fiabilité au quotidien, surtout sur des profils ayant connu une migration ou plusieurs appareils.


Et ensuite

L'app mobile est en finalisation. La sortie est prévue d'ici un à deux mois, comme annoncé dans le point sur le cap des 1 000 utilisateurs.

Les prochains chantiers visibles : l'onboarding mobile (premier lancement, jumelage avec le desktop, phrase de récupération BIP-39), l'écran des fichiers et la navigation dans les dossiers, et les derniers cas de sync à couvrir. Le graphe de notes est posé, le chiffrement par KEK et FEK par fichier tourne, l'AES-256-GCM est en place. Ce qui reste, c'est du polish et des cas d'erreur à gérer honnêtement.

Cette semaine a montré que "presque fini" peut cacher pas mal de travail. On ne shippera pas un truc instable.

Prochain point d'avancement la semaine prochaine.

#changelog#release#mobile#sync#build-in-public#react-native

Articles liés