Yuno15

Projet clientPWAHors-ligne

Une app terrain qui fonctionne sans réseau.

Le technicien remplit son rapport sur le toit, en mode avion. Le bureau le reçoit le jour même et l'envoie au client en PDF.

Stack

  • PWA
  • JavaScript
  • Supabase
  • PostgreSQL
  • Python
  • FastAPI
  • Claude
Le même tableau de bord au même instant sur MacBook Pro en thème sombre et iPad en thème clair
Le même tableau de bord au même instant : le bureau en sombre sur Mac, le technicien en clair sur iPad. Captures de l'app en production, en mode démonstration : marque, lieux et données neutralisés.
69 invariants
Des tests nés chacun d'un bug de production, qui lisent le code livré.
Le jour même
le bureau voit l'intervention, contre trois jours de ressaisie auparavant.
Mode avion
pour tout le relevé. Seule la synchronisation a besoin du réseau.
1 minute
entre deux lectures des boîtes mail par l'agent. Chaque écriture attend une validation humaine.

Interfaces

Le téléphone sur le toit, le tableau de bord au bureau.

Les deux écrans lisent la même base. Ce que le technicien valide apparaît sur la carte du bureau dès que son téléphone retrouve du réseau.

Le téléphone du technicien
Le téléphone du technicien
Le tableau de bord du bureau
Le tableau de bord du bureau
À gauche, l'accueil du technicien : l'intervention en cours, l'itinéraire, les suivantes. À droite, le bureau : compteurs du jour, équipes sur la carte, planning du mois. Captures de l'app en production, mode démonstration. Interface en anglais, lieux et données neutralisés.

Relecture

L'agent propose, une personne valide.

Devis signés, factures et réclamations arrivent par mail. L’agent en extrait les champs, chacun reste modifiable, et rien n’est écrit dans la base avant le clic sur le bouton de droite. Interface reconstituée, données fictives.

Fonctionnalités

Du relevé au rapport client.

Quinze étapes au maximum par relevé, et seulement celles qui concernent le type d'intervention.

  • Hors-ligne d'abord

    Chaque saisie s'écrit d'abord sur le téléphone et rejoint une file locale. L'enregistrement n'attend jamais le réseau.

  • Fusion champ par champ

    Le technicien hors-ligne et le bureau en direct modifient la même fiche. Une règle écrite décide qui gagne, et un conflit est toujours signalé.

  • Appareil photo intégré

    La prise de vue passe par le navigateur, dans l'app. Sur les Android durcis du terrain, l'appareil photo du système refusait souvent de s'ouvrir.

  • Rapport client en PDF

    Le bureau génère le rapport en un geste, à partir des champs et des photos du relevé.

  • Cloisonnement par entreprise

    Chaque ligne de la base appartient à une entreprise (Row Level Security de PostgreSQL). Un contrôle le vérifie avec un vrai compte de technicien.

  • Sauvegardes chiffrées

    Des copies chiffrées hors machine, et une restauration vérifiée octet par octet. L'app s'installe depuis le navigateur, sans magasin d'applications.

Architecture

Le trajet d'un relevé.

Seule la quatrième étape touche au réseau. Tout ce qui précède fonctionne en mode avion.

  1. 01 Sur le toit Le technicien remplit, photographie et valide les étapes de son type d'intervention.
  2. 02 Écrit sur le téléphone Immédiatement : la fiche dans le stockage local, les photos dans une base locale dédiée.
  3. 03 La file La modification attend son tour, avec un numéro de révision qui change à chaque retouche.
  4. 04 Le réseau revient La file se rejoue dans l'ordre. Avant chaque écriture, elle relit la ligne du serveur et fusionne champ par champ.
  5. 05 Le bureau La carte, les compteurs du jour et le planning se mettent à jour, et le rapport client sort en PDF.

Agent mail

Un agent en lecture seule sur la base client.

Il lit deux boîtes mail chaque minute et prépare des propositions. Sa seule écriture autorisée : passer le statut d'une intervention à « planifié ».

Ce que l'agent fait seul

  • Lire deux boîtes mail toutes les minutes et classer chaque message
  • Ouvrir les PDF joints et en extraire client, références, montants et dates
  • Interroger la base du client en lecture seule
  • Rédiger une réponse, laissée non envoyée

Ce qu'il n'a pas le droit de faire

  • Écrire dans la base du client
  • Envoyer un mail
  • Créer une fiche que personne n’a relue
  • Supprimer ou écraser une donnée existante

Incidents de production

Quatre pannes, et ce qu'elles ont changé.

Chacune a laissé un contrôle écrit, qu'on peut rejouer.

01 · Brouillon perdu

Un technicien a perdu un rapport déjà rempli

Juillet, au retour d'un chantier

Ce qu’on croyait
Le brouillon était sauvé à chaque saisie et à chaque photo. Revenir dans l'app devait le retrouver.
Ce qui s’est passé
Il est sorti de l'app pendant un rapport. En revenant, il était sur l'accueil, et les photos du brouillon avaient disparu.
Pourquoi
Cinq défauts se sont enchaînés : un cache qui servait une version périmée, un stockage de photos devenu privé, une course entre deux opérations, des adresses d'images expirées et une synchronisation fragile. Le brouillon existait, mais le rouvrir demandait un bouton qu'il n'a jamais vu, et la photo s'écrivait encore dans la base locale quand il a quitté l'app.
Ce qu’on a changé
Rouvrir l'app sur un brouillon actif rouvre le rapport à l'étape sauvée. Le brouillon attend la fin de l'écriture de ses photos avant d'être sauvé, et il est aussi sauvé quand on quitte l'app.
La preuve
Des tests d'invariants couvrent la chaîne en lisant la source livrée. La version en ligne se vérifie en téléchargeant le service worker déployé.
02 · Photos invisibles

Toutes les photos ont disparu, sans aucune donnée perdue

Juillet, signalé comme « pas de photos sur une intervention »

Ce qu’on croyait
Le stockage des photos est privé : l'app signe des adresses temporaires pour les afficher, et garde une adresse publique en repli.
Ce qui s’est passé
Toutes les photos ont disparu en même temps, pour tous les utilisateurs, sans message d'erreur.
Pourquoi
Sur un stockage privé, une adresse publique renvoie une erreur : le repli n'avait jamais fonctionné. Toutes les adresses étaient signées en un seul appel groupé, dont l'échec était ignoré. Une erreur réseau passagère vidait donc tout l'affichage, alors que chaque fichier et chaque ligne étaient intacts.
Ce qu’on a changé
Les adresses signées valides sont gardées d'un rechargement à l'autre, l'affichage repart d'elles au démarrage, et la signature se fait par petits lots isolés : un lot en échec ne bloque que ses propres photos.
La preuve
Un diagnostic en trois couches, écrit et rejouable : la ligne existe-t-elle, le fichier existe-t-il, la signature fonctionne-t-elle. Trois oui signifient que rien n'est perdu et que le défaut est dans l'affichage.
03 · File de synchronisation

Une saisie faite pendant une synchronisation disparaissait

Juillet, trouvé par un audit adverse

Ce qu’on croyait
Envoyer les fiches en attente une par une, puis réécrire ce qui reste dans la file. Le code tournait ainsi depuis des mois.
Ce qui s’est passé
Ce qui était tapé pendant une synchronisation pouvait être perdu. Le serveur avait une version, l'écran une autre, et plus aucun envoi n'était prévu pour rattraper l'écart.
Pourquoi
Lire la file, envoyer, puis réécrire la file : le réseau sépare ces trois moments, et la réécriture effaçait les entrées ajoutées entre-temps. L'horodatage existant ne pouvait pas le détecter, car il est volontairement conservé au ré-enfilage pour la détection de conflit.
Ce qu’on a changé
Chaque entrée porte un compteur de révision qui change à chaque retouche. À l'acquittement, la file est relue, et le résultat d'un envoi n'est appliqué que si la révision n'a pas changé. Ce qui a été ajouté pendant l'envoi reste dans la file.
La preuve
La règle de fusion vit entre deux marqueurs dans la source, et un script teste exactement cette zone : déplacer le code sans déplacer le test fait échouer le script.
04 · Faux succès

Un bouton affichait « c'est fait » et ne supprimait rien

Trouvé en écrivant un test contre la vraie base

Ce qu’on croyait
La base isole les données par entreprise, ligne par ligne. Les règles étaient écrites et relues.
Ce qui s’est passé
Un bouton de remise à zéro annonçait la réussite à chaque fois et ne retirait aucune ligne, pendant des semaines. Rien n'échouait, donc personne ne l'a vu.
Pourquoi
Quand une suppression ne correspond à aucune règle de sécurité, PostgreSQL ne renvoie pas d'erreur : la requête réussit et supprime zéro ligne. Vu de l'app, un refus silencieux ressemble exactement à un succès.
Ce qu’on a changé
L'isolation se vérifie avec un vrai compte de technicien sur la vraie base : le contrôle crée une fiche jetable et teste ce que ce compte peut atteindre et ce qu'il ne peut pas.
La preuve
Le contrôle tourne avec les droits d'un technicien et échoue si une règle manque, le cas même pour lequel il a été écrit.

Décisions produit

Deux diagnostics tranchés par une mesure.

Avant de corriger le code, on mesure ce qui se passe vraiment.

Doublons d'interventions

Mesuré

0 sur 55

identifiants en double, et zéro identifiant partagé sur 181 événements d'agenda. La déduplication fonctionnait.

Décidé

En amont

Les doublons venaient d'agendas où un rendez-vous supprimé puis recréé reçoit un nouvel identifiant. Le correctif a été fait à la source, la fusion n'a pas changé.

Couverture des tests

Mesuré

3 zones

testées : démarrage, service worker, hors-ligne. Un blocage total à l'étape 5 est passé pendant des semaines.

Décidé

Entrée = sortie

Un contrôle vérifie que ce qui est saisi ressort dans le rapport. À son ajout, il a trouvé cinq défauts en une passe.

Rôle
Conception produit, UX terrain, développement full-stack
Cadre
Projet client · anonymisé · 2026
Résultat
69 tests d'invariants, chacun né d'un bug de production. Le bureau voit l'intervention le jour même au lieu de trois jours plus tard.