Étude de cas · En production
Inventaire d'atelier industriel
Les opérateurs d'un atelier de rectification consultaient l'état de 446 outils dans un ERP lourd, installé sur un seul poste. J'ai construit l'interface web qui expose ces données à tous les postes, et automatise les commandes de réapprovisionnement.
- Rôle
- Conception, développement et déploiement — seul
- Durée
- Juillet 2026 — en service depuis
- Volume
- 8 750 lignes applicatives, 899 lignes de tests
- Données
- 446 outils, 113 clients, synchronisés depuis l'ERP
- Stack
- PHP 8 · Python 3 · JavaScript · Tailwind · ReportLab · SMTP
- Contrainte
- Windows Server hors ligne, aucune base de données installable
Le problème
L'atelier gère son outillage dans Clipper, un ERP industriel propriétaire. Consulter l'emplacement physique d'une meule, savoir si un outil client est disponible ou au rebut, demander un rachat : tout passait par un poste unique, sous licence, avec une interface conçue pour la saisie comptable et non pour un opérateur devant sa machine.
En pratique, les opérateurs se déplaçaient, ou demandaient à un collègue. Les demandes de réapprovisionnement partaient par e-mail manuel, une par une, sans trace consolidée.
Trois contraintes non négociables encadraient la solution :
- Aucune modification de Clipper. L'ERP est le système de référence comptable ; y écrire depuis un outil tiers était exclu. Le seul point d'entrée disponible est un export XML déposé sur un partage réseau.
- Aucune base de données. Le serveur est un poste Windows de l'atelier, sans SGBD installé et sans budget ni administrateur pour en maintenir un.
- Aucun accès Internet entrant. Le service ne devait être joignable que depuis le réseau local.
L'architecture
Le flux est volontairement unidirectionnel : Clipper reste la source de vérité, l'application n'écrit jamais en retour. Elle transforme, affiche, et notifie par e-mail.
Décisions techniques
Fichiers JSON verrouillés par flock(), plutôt qu'un SGBD
Le serveur ne pouvait pas héberger de base de données. J'ai retenu des fichiers JSON dont
chaque écriture passe par un verrou exclusif : ouverture en c+,
LOCK_EX, lecture par fread() à l'intérieur du verrou,
ftruncate(), réécriture, fflush() avant déverrouillage.
L'ordre compte : déverrouiller avant le vidage du tampon romprait l'atomicité, et le
bug serait invisible jusqu'à la première collision réelle.
Trois points d'entrée manipulent le même fichier de commandes. Plutôt que de répéter la séquence trois fois — et de risquer qu'une copie dérive — je l'ai factorisée dans un helper qui prend une fonction de mutation : l'appelant décrit la transformation, le helper garantit le verrou.
Un seul langage écrit les fichiers d'état
Écriture directe depuis Python
Le batch quotidien est écrit en Python et doit marquer des commandes comme envoyées.
Le réflexe serait d'écrire le JSON directement. Je ne l'ai pas fait : le module
fcntl de Python n'existe pas sous Windows, et rien ne garantit qu'un verrou
posé côté Python s'entende avec le flock() de PHP sur la même plateforme.
Le script Python appelle donc un point d'entrée PHP en ligne de commande pour toute écriture. Un seul langage écrit, avec un seul mécanisme de verrouillage. Le coût est un processus supplémentaire par exécution ; le gain est l'absence de corruption silencieuse.
Chemins de l'ERP surchargeables par variables d'environnement
Chemins codés en durLes exports vivent sur un partage Windows au chemin figé. Les coder en dur rendait le projet intestable ailleurs que sur le serveur de production. Deux variables d'environnement les surchargent, avec repli sur les valeurs réelles : le comportement en production est inchangé, mais le développement et les tests deviennent possibles sur une autre machine.
Difficultés rencontrées
Des fichiers XML qui mentent sur leur encodage
Les exports déclarent encoding="ISO-8859-1" dans leur en-tête. L'un d'eux
respecte cette déclaration, un autre non. Résultat : selon le fichier, soit les
accents s'affichaient correctement, soit ils devenaient des caractères parasites — et
faire confiance à l'en-tête cassait systématiquement l'un des deux cas.
J'ai écrit une fonction de chargement qui détecte l'encodage réel au lieu de croire la déclaration, puis l'ai couverte par des tests qui construisent les octets bruts directement en mémoire. Utiliser des fichiers de test aurait réintroduit le problème : l'outil qui les écrit choisit son propre encodage, et le test aurait validé autre chose que le cas réel.
Un regroupement qui s'effondrait sur une catégorie
L'envoi groupé range les commandes par fournisseur. Les meules portent un nom de fournisseur ; les diamants, dans l'export Clipper, n'en ont pas — le champ est toujours vide. Sans traitement particulier, toutes les commandes de diamants tombaient dans un unique groupe générique et partaient au mauvais destinataire.
Le correctif est un repli sur le code court du fournisseur quand le nom complet manque. Le cas est couvert par un test dédié, parce que c'est exactement le genre de régression qui repasse inaperçue après une refonte.
Un « valeur par défaut » qui s'affichait dans les PDF
Les bons de commande affichaient littéralement --- à la place des
observations vides. La fonction de formatage traitait une chaîne vide passée
explicitement comme une absence de valeur, et substituait son marqueur de remplissage.
Bug mineur, mais visible par le fournisseur sur un document officiel.
Un incident de sécurité, et sa correction
La première version stockait les identifiants SMTP dans un fichier .env
placé à la racine du site. C'est une faute : le serveur intégré de PHP sert
n'importe quel fichier de son dossier, fichiers cachés compris. Toute
personne du réseau connaissant l'URL pouvait télécharger le mot de passe en clair.
Corrections apportées :
- Le fichier de secrets a été déplacé un niveau au-dessus de la racine web, hors de portée du serveur.
- Le script d'installation le crée avec une saisie masquée, et ne le réécrit jamais s'il existe déjà.
- Le mot de passe précédent a été considéré comme compromis et révoqué : la documentation impose d'en générer un nouveau plutôt que de réutiliser l'ancien.
- L'authentification utilise un mot de passe d'application dédié, jamais le mot de passe principal du compte.
Je documente cet incident plutôt que de le taire : la faute initiale est réelle, la question qui compte est de savoir si elle a été comprise et traitée correctement. Considérer l'ancien secret comme compromis plutôt que de simplement le déplacer est la partie non-évidente — et la seule qui protège réellement.
Résultats
446
outils consultables depuis tous les postes
113
fiches clients gérées
1/jour
envoi groupé au lieu d'e-mails manuels
899
lignes de tests (Python + PHP)
L'application tourne en continu depuis juillet 2026, relancée automatiquement au démarrage du serveur par une tâche planifiée. Les demandes de rachat de la journée partent groupées par fournisseur à 17h30, accompagnées d'un PDF généré avec l'en-tête et le tampon de l'entreprise. En cas d'échec, les commandes restent en attente et sont reprises au passage suivant : aucune demande n'est perdue.
Ce que je referais autrement
SQLite plutôt que des fichiers JSON. J'avais écarté toute base de données à cause de la contrainte d'installation — à tort : SQLite n'est pas un serveur, c'est un fichier, et l'extension est fournie avec PHP. J'aurais eu les transactions et le verrouillage sans écrire une ligne de gestion de concurrence. C'est la décision que je défends le moins aujourd'hui.
Le serveur intégré de PHP traite une requête à la fois. Cela suffit pour quelques postes, mais ne tient pas si l'usage s'étend. La documentation signale la limite et pointe vers Apache ; ce n'est pas encore fait.
Les journaux ne sont pas purgés. Un fichier par jour s'accumule indéfiniment. Une rotation aurait coûté quelques lignes.