D’où vient cet outil
Dans un parking en sous-sol, j’avais besoin de remplir une formalité administrative. J’ai rempli le formulaire hors réseau, la page était déjà chargée. Une fois sorti du parking, je l’ai envoyé, et il a refusé mon contenu parce que j’avais attendu trop longtemps. Le jeton anti-bot avait expiré.
J’avais tout bien fait, et l’outil l’a jeté pour une contre-mesure technique dont je ne suis pas censé me soucier.
Le problème
Cette démonstration part de là. Une métallerie de 15 personnes tiendrait son magasin dans un classeur partagé. 64 références : tubes, cornières, bobines de fil, disques, visserie, bouteilles de gaz. Deux magasiniers saisiraient dedans. Le gérant regarderait la colonne « stock » pour décider quoi commander.
La colonne mentirait, pour trois raisons qui reviennent dans tous les classeurs partagés :
- quelqu’un prend cinq mètres de fer plat sans rien noter ;
- deux personnes ouvrent le fichier en même temps, et la dernière à enregistrer efface le travail de l’autre ;
- un rechargement malheureux ramène une version d’avant-hier.
À l’inventaire annuel, personne ne saurait expliquer les écarts. Et ça coûterait bien avant l’inventaire : une bobine de fil fourré manquante un mardi matin, deux soudeurs qui attendent.
Ce qui change au quotidien
Le stock affiché correspond à ce qu’il y a sur l’étagère. Quand ce n’est plus le cas, l’inventaire dit de combien et depuis quand. C’est toute la différence entre commander au jugé et commander sur un chiffre.
Saisir se fait debout, une main sur le téléphone, l’autre sur la pièce : un code, un sens, une quantité, un motif dans une liste courte. Un magasin en sous-sol, entre des rayonnages métalliques, c’est là où le réseau tombe. Ça marche quand même, et j’y reviens plus bas parce que c’est le cœur du sujet.
Chaque référence porte un seuil, et le gérant est prévenu quand elle passe dessous, sans avoir à ouvrir quoi que ce soit.
L’installation tient en deux conteneurs et une commande, sans abonnement. Rien ne sort de la machine : aucune police d’écriture téléchargée ailleurs, aucune mesure d’audience, aucun service extérieur, et la base de données n’ouvre même pas de porte vers l’extérieur.
Comment ça marche
Deux faits se contredisent : le sous-sol coupe le réseau, et deux magasiniers travaillent sur les mêmes références.
Si le téléphone accepte une sortie sans réseau, il l’accepte sans pouvoir demander au serveur s’il reste de la marchandise. Prenez trois joints toriques en rayon. Deux personnes en sortent trois chacune. Le stock tombe à moins trois.
On ne peut pas avoir -1 bobine de fil de 15 kg. Logique dans la réalité, possible dans une application, et ça ne doit pas l’être. On reflète la réalité.
Aucun réglage ne fait disparaître ça. Il faut choisir quel geste on protège, et le dire à l’écran.
J’ai préparé le PWA avant tout, mais je savais que la problématique était différente de mon expérience dans le parking. Si on doit pouvoir tout faire sans réseau, alors une entrée doit toujours être conservée et passer, un comptage aussi, mais une sortie peut être refusée si, quand on revient dans le réseau, le stock est à zéro.
Le choix suit le sens physique du geste :
- Une entrée passe toujours. De la marchandise arrive, elle ne peut pas manquer. Le téléphone l’enregistre définitivement, réseau ou pas.
- Un comptage d’inventaire aussi. Il décrit ce que quelqu’un a vu sur une étagère, il ne retire rien à personne.
- Une sortie sans réseau est provisoire. L’écran le dit avant le geste, et le serveur pourra la refuser si un collègue a pris les mêmes pièces.
Ce que le serveur refuse ne disparaît pas : ça atterrit dans un écran de rattrapage. Ces gestes ont eu lieu. Les pièces ont quitté le rayon, et c’est à quelqu’un de dire ce qu’on en fait. Un outil qui les avalerait en silence fabriquerait l’écart inexplicable qu’on cherchait à supprimer.
Reste le cas que personne n’anticipe. Le téléphone envoie sa saisie, le serveur l’enregistre, et la réponse se perd en route. Le téléphone croit avoir échoué et renvoie. Chaque saisie porte donc un numéro unique, et un envoi parti deux fois ne compte qu’une fois.
Ce que vous pouvez vérifier sans moi
Vu du dehors, rien ne distingue une application sérieuse d’un formulaire qui écrit dans un fichier. Les deux montrent des champs et un bouton. Lire le code trancherait, sauf que personne ne lit le code.
L’outil embarque donc un écran qui exécute ses propres preuves devant vous. Cinq boutons, cinq situations qui font peur :
- deux magasiniers sortent la dernière pièce au même instant ;
- la même saisie part deux fois ;
- quelqu’un tente de réécrire une ligne du registre ;
- un magasinier essaie de valider un inventaire à la place du gérant ;
- le stock est recalculé de bout en bout, pour vérifier qu’il n’a pas dérivé.
Chacun tourne pour de vrai. L’écran affiche la réponse de la base telle quelle, message d’erreur compris, avec le temps que ça a pris. La course entre les deux magasiniers se règle en une quarantaine de millisecondes. Les deux essais qui écrivent laissent leur trace horodatée dans le registre, à côté des mouvements de la semaine.
30 secondes, sans savoir programmer. Chez un client, un réglage fait disparaître cet écran.
23 tests automatiques tournent par ailleurs sur l’installation complète. Cinq pilotent un vrai navigateur : ils coupent son réseau, saisissent une sortie, remettent le réseau, et vérifient que tout repart seul.
Ses limites
Le scan est plus lent sur iPhone. Safari ne sait pas lire les codes-barres tout seul, donc l’application télécharge un lecteur de secours de 230 kilo-octets, et seulement sur iPhone. Comptez une seconde de plus. Sans doute deux sur un appareil qui a quelques années. De toute façon la moitié du magasin se saisit à la main, puisqu’une barre d’acier débitée à la longueur arrive sans étiquette.
Un seul magasin, un seul emplacement par article. Gérer plusieurs emplacements demanderait de reprendre chaque écran et chaque recherche. J’ai préféré tenir un seul flux jusqu’au bout.
Aucune commande fournisseur. Recevoir contre un bon de livraison, gérer un reliquat, rapprocher une facture : c’est un autre métier. La valeur du stock retient le dernier prix d’achat connu. La comptabilité analytique commence là où cet outil s’arrête.
Un détail sur les téléphones partagés. Pour fonctionner sans réseau, l’application garde une copie de la page en mémoire, et cette copie porte le nom de la personne connectée. Quelqu’un peut donc l’apercevoir après une déconnexion, une fraction de seconde, avant que la page fraîche arrive. La copie est effacée au retour sur l’écran de connexion. Aucune donnée de stock n’y figure.
Sous le capot
Cette section s’adresse au développeur qui vous accompagne, s’il y en a un. Vous pouvez la sauter sans rien perdre.
Le stock est la somme d’un journal en ajout seul, tenue par un déclencheur
PostgreSQL, sous une contrainte qui interdit le négatif. Une erreur de saisie se corrige
par un mouvement inverse. Rien ne réécrit une ligne passée. Deux
sorties simultanées sur la même référence se mettent en file sur le verrou de
ligne au lieu de s’ignorer. Aucune vérification écrite dans l’application ne
produit ce résultat : entre un SELECT et un INSERT subsiste toujours une
fenêtre, et c’est précisément là que deux magasiniers se croisent.
L’interface ne détient aucune des règles qui comptent. Six déclencheurs, dix
contraintes de vérification et deux index uniques vivent en base, donc ils
valent aussi devant psql et devant le script d’import que quelqu’un écrira un
jour. L’écran de preuves lit le catalogue de PostgreSQL pour afficher ce que la
base contient vraiment : une règle retirée du SQL disparaît de cet écran.
Deux choses apprises en construisant.
Le déclencheur qui applique un mouvement s’écrivait naturellement en
INSERT ... ON CONFLICT DO UPDATE. Il était faux. PostgreSQL évalue les
contraintes CHECK sur le tuple proposé à l’insertion, avant de détecter le
conflit et de basculer sur la branche de mise à jour ; or ce tuple porte le
delta brut, négatif pour une sortie. La première sortie d’un article était donc
refusée alors que son stock valait 13. Il a fallu écrire les 600
mouvements du jeu d’exemple pour s’en apercevoir.
Le conteneur, lui, a corrigé ce que la machine laissait passer. Sans
.dockerignore, le COPY de contexte recouvre l’arbre de liens symboliques
que pnpm vient d’installer, et la construction meurt sur un module introuvable.
Une application qui compile sur un portable et une image qui démarre restent
deux choses différentes.
L’essayer
Ouvrez la démonstration, les identifiants sont affichés sur la page de connexion. Allez dans l’onglet « Sous le capot » et appuyez sur les cinq boutons.
Puis coupez le réseau de votre téléphone et continuez à travailler.
