02 — Projet personnel
Vingt-deux mille lignes, sept dépendances, sur une machine à 40 €
Une application pour la famille — menus de la semaine, recettes, liste de courses partagée en direct — qui tourne sur un petit ordinateur à 40 € branché à la maison. Écrite entièrement de zéro, et testée automatiquement.
Cinq personnes à la maison, une question par jour — qu'est-ce qu'on mange ? — et une liste de courses jamais à jour. Les applications du marché demandent un compte, un abonnement, et envoient les données de la famille chez un tiers. Aucune ne fait ce dont on avait besoin : que la liste se coche en direct sur tous les téléphones, dans l'ordre des rayons du magasin où l'on se trouve.
Le problème
Le besoin est trivial à énoncer et difficile à satisfaire : l'application doit être plus simple que le problème qu'elle résout. À la première friction — un compte à créer, un mot de passe oublié, un réglage à comprendre — la famille retourne au papier, et le projet est mort. Le public est impitoyable et ne fera aucun effort.
Deuxième difficulté : la machine. Un Raspberry Pi 3B, c'est un gigaoctet de mémoire, un processeur ARM et une carte SD qui s'use à chaque écriture. La plupart des choix par défaut du web moderne y sont inapplicables — et c'est précisément ce qui rend l'exercice formateur : on ne peut résoudre aucun problème en ajoutant des ressources.
Ce que j’ai construit
Une application web installable sur l'écran d'accueil, qui couvre neuf repas par semaine et met en avant le prochain repas — c'est la question qu'on se pose vraiment en ouvrant l'application.
La liste de courses se construit à partir du menu, se recalcule à chaque changement, agrège les quantités entre les repas, et se trie dans l'ordre de parcours réel du magasin. Quand quelqu'un coche un article, la case se coche sur les autres téléphones dans la seconde, avec la photo de qui l'a pris. Le catalogue du drive est collecté chaque semaine pour rattacher chaque ingrédient à un vrai produit, avec son prix et son conditionnement.
Pour les recettes, le parcours a été inversé en cours de route : au départ l'IA inventait le plat et la famille découvrait sa liste de courses. Maintenant la famille écrit les ingrédients, et l'IA se contente de rédiger et de combler les trous.
Décisions structurantes
01Sept modules volontairement purs, et chacun explique pourquoi
Composition du menu, agrégation des courses, calcul des conditionnements, fusion des ingrédients, unités, dates, équipements : sept modules sans base, sans réseau, sans horloge. L'aléa et le temps sont injectés en paramètres, ce qui rend deux cents tirages parfaitement reproductibles. Chacun porte l'explication de sa pureté en commentaire — pour l'un d'eux : « c'est là que se cachent les erreurs de calcul qu'on ne verrait qu'au supermarché ». C'est la différence entre écrire des tests et concevoir son code pour être testable.
02La frontière avec l’IA est tenue par une fonction, pas par le prompt
Trois règles : ce que la famille a écrit n'est jamais réécrit ; l'IA ne remplit que les trous ; ses ajouts sont systématiquement marqués « placard », quoi qu'elle en dise, et plafonnés à six. Si elle déborde et ajoute du poulet, il sera marqué placard lui aussi — la liste de courses ne doit pas grossir dans le dos de la famille. Cette frontière est une fonction pure et testée hors ligne, parce que « ce qu'on dit exactement à l'IA compte trop pour n'être vérifiable qu'en la payant pour voir ».
03Le tirage du menu est un score, pas un hasard
La pénalité de récence est continue sur cinq semaines — 0,02 les sept premiers jours, puis une remontée linéaire jusqu'à disparaître au trente-cinquième — et un plat jamais servi reçoit un bonus pour faire tourner le répertoire. La préférence de jour est continue elle aussi, pas par paliers : avec un seuil, un plat d'une heure et un de deux heures se vaudraient. Et le garde-fou le plus subtil : un plat dont la durée est inconnue ne doit ni gagner ni perdre — sinon toute recette non renseignée passerait pour rapide et raflerait les soirs de semaine, non parce qu'elle est courte mais parce qu'on ignore sa durée.
04Les quantités ne s’additionnent que quand c’est honnête
La clé de fusion est le nom normalisé plus la famille d'unité. Deux paquets s'additionnent ; un paquet et une boîte, jamais. Et surtout : chaque ingrédient porte deux quantités, celle de la recette et celle qu'on met au panier — « six tranches de jambon » sur une liste de courses se lit « six jambons », alors qu'au magasin on prend un paquet. Le calcul du nombre de paquets déduit la contenance du prix au kilo affiché, avec une marge de 2 % pour ne pas acheter un paquet de plus à cause de trois grammes.
05Un registre d’arrêt propre pour les flux temps réel
Le temps réel passe par un flux HTTP unidirectionnel — du HTTP ordinaire, qui traverse le tunnel sans configuration et que le navigateur reconnecte tout seul. Mais un tel flux ne se termine jamais de lui-même : le serveur restait bloqué quatre-vingt-dix secondes à chaque redémarrage dès qu'un téléphone avait la liste ouverte, puis le gestionnaire de services le tuait — soit une coupure à chaque mise à jour, au pire moment. Un registre des flux ouverts, fermé sur signal d'arrêt, a réglé le problème. Avec battement de cœur toutes les 25 secondes contre les proxys, expiration à 30 minutes, et fermeture idempotente sur trois déclencheurs.
06Aucun mot de passe, et une preuve de travail à la place du captcha
Chacun reçoit un lien personnel à ouvrir une fois ; la session dure 400 jours — le maximum qu'acceptent les navigateurs. Le jeton de session est haché avant stockage : une base volée ne donne aucune session. Coller un lien à la main exige d'abord de résoudre un calcul de 16 bits de zéros en tête — invisible pour un humain, facturé au robot. Un captcha à images serait un obstacle pour un parent de 61 ans et ne coûterait presque rien à un robot moderne ; ici c'est l'inverse. Et la lucidité est écrite dans le code : ce n'est pas la défense principale, la limitation par adresse l'est.
07Le Raspberry Pi ne compile jamais rien, et la bascule se vérifie
Une compilation sur cette machine prend dix à vingt minutes et use la carte SD. L'intégration continue compile ailleurs en une minute et publie sur une branche dédiée ; le Pi ne fait que récupérer. Le workflow échoue volontairement s'il détecte un binaire natif dans le paquet — un artefact x86 sur un ARM ne se voit qu'au moment où le service refuse de démarrer, à distance, un dimanche soir. Et la mise à jour interroge réellement l'application après la bascule : si elle ne répond pas, la version précédente est restaurée.
08Le cache est un filet, pas une optimisation
Le service worker applique trois stratégies selon la nature de la ressource, avec cinq exclusions chirurgicales — dont le flux temps réel, et l'écran des réglages exclu pour raison de sécurité : les liens d'accès de la famille ne doivent pas rester sur le disque du téléphone. Pas de revalidation en arrière-plan, délibérément : une liste de courses fausse serait pire que pas de liste du tout. La page hors ligne est embarquée dans le service worker, servie en 503, et reproduit le design system — thème sombre compris.
Le morceau dont je suis le plus fier
Tester sans framework
Zéro dépendance de test : ni bibliothèque d'assertions, ni pilote de navigateur. Le micro-framework fait douze lignes. Et pourtant 372 assertions sur 37 sections.
Les tests d'algorithme ne vérifient pas des égalités mais des propriétés statistiques sur des centaines de tirages, avec un générateur pseudo-aléatoire écrit à la main et ensemencé — donc un échec est rejouable à l'identique. Exemples : la part de plats rapides en semaine dépasse 50 %, avec le plafond théorique atteignable calculé et commenté dans le test ; l'écart de durée moyenne entre week-end et semaine dépasse vingt minutes ; sur 300 tirages et 2 700 créneaux, zéro jour avec le même plat midi et soir.
Les tests de bout en bout, eux, appellent les vraies actions serveur : ils lisent le manifeste généré par le framework, y résolvent l'identifiant interne de chaque action, envoient la requête sur la vraie page avec l'en-tête attendu, et décodent le flux de sérialisation React pour récupérer la valeur de retour. Aucun bouchon, aucun navigateur : le vrai chemin d'écriture, session et middleware compris. Le plus beau cas teste la synchronisation entre deux clients distincts — un flux ouvert d'un côté, une action déclenchée de l'autre, et une attente simultanée de l'évènement et du retour.
Trois problèmes qu’il a fallu diagnostiquer
Là où ça devient intéressant
L'empreinte TLS. Le catalogue du magasin refusait mes requêtes. Après avoir construit méthodiquement le bon jeu d'en-têtes — le tableau d'essai est resté dans le dépôt — je tombe sur un cas impossible : un outil en ligne de commande refusé là où un client Python passait, avec exactement les mêmes en-têtes. La différence n'était pas dans ce qui est envoyé mais dans la signature de la poignée de main chiffrée. Conclusion : ce module reste en Python alors que tout le reste est en JavaScript, avec le raisonnement écrit pour celui qui voudra « harmoniser la stack ».
La table de partitions écrite en dernier. Windows monte automatiquement toute partition qu'il reconnaît sur un support amovible et s'approprie la zone, faisant échouer les écritures suivantes. Les deux parades habituelles ne marchent pas. La solution est une inversion de l'ordre des opérations déduite du comportement du système : écrire le corps de l'image d'abord, les quatre premiers mégaoctets à la fin. Avec onze contrôles avant la moindre écriture, et une vérification par relecture — parce qu'un flux qui se termine sans erreur ne prouve pas que les octets sont arrivés sur le support.
Une bibliothèque qui supprime une fonctionnalité en silence. La version utilisée pour la passerelle de messagerie a commenté son bloc de traitement des votes de sondage, avec la mention « à retirer entièrement » : l'évènement n'arrive plus, sans erreur ni avertissement. J'ai réimplémenté les lignes retirées à partir des primitives encore exposées — et corrigé au passage trois problèmes que la bibliothèque n'aurait pas résolus, dont le fait que son agrégation ne dédoublonnait pas par votant : quelqu'un passant de « oui » à « non » comptait pour les deux.
Le soin, dans le détail
- Les écrans de chargement sont des composants serveur — zéro JavaScript pour un état d'attente — avec des largeurs de lignes volontairement irrégulières pour ne pas trahir le gabarit, et les dimensions exactes du contenu à venir.
- La mise à jour optimiste est écrite à la main, avec un retour arrière ciblé sur le champ concerné de la ligne concernée : annuler le tableau entier écraserait les évènements temps réel arrivés entre-temps. Aucune primitive du framework ne sait faire ça.
- Le clavier virtuel est géré par trois mécanismes empilés : recalage sur la fenêtre visuelle, défilement différé de 300 ms (mesurer avant, c'est mesurer l'ancienne hauteur), et des espaceurs en fin de formulaire — parce que centrer le dernier champ est impossible s'il n'y a physiquement rien en dessous.
- Huit lignes expliquent pourquoi il n'y a pas de flou d'arrière-plan sur la barre de navigation : sur un moteur de rendu, un élément fixe qui en porte un est composé à part et se repositionne au défilement. La leçon a été propagée à un second endroit du code.
- Toutes les dates sont mises en français côté serveur. Zéro formatage de date côté client dans tout le projet — un téléphone mal réglé afficherait un autre jour que celui du repas.
- La réduction de mouvement est correctement écrite : durée quasi nulle et nombre d'itérations forcé à un — sans quoi une animation infinie à 0,01 ms boucle des milliers de fois par seconde.
- Chaque régression a laissé un test derrière elle, avec son histoire en commentaire. Le plus savoureux : des limites de mot écrites une fois en caractères de contrôle, ce qui rendait les expressions incapables de reconnaître quoi que ce soit. Une expression qui compile, ne lève jamais, et ne correspond à rien. Sans test, ça ne se serait vu qu'au supermarché.
Chiffres
hors dépendances et fichiers générés
hors outils de développement
schéma déclaré dans le code
comptage des exports
1 771 lignes sur 12 479
directive server-only
maximum accepté par les navigateurs
Ce que je ferais évoluerDéplier
- Le collecteur de catalogue franchit une règle d'exclusion du site : les fiches produit sont autorisées, la pagination ne l'est pas. Usage strictement personnel, passe hebdomadaire, débit bridé volontairement — c'est écrit dans une section « limites assumées » du dépôt, avec la mention que le bridage n'est pas négociable. Je préfère le dire que le laisser trouver.
- Le retour arrière restaure le code, pas la base. Une migration malheureuse serait irréversible. C'est le défaut le plus sérieux du dispositif, et le plus simple à corriger.
- Le script de sauvegarde n'est câblé à rien — et il écrit sur la carte SD, donc ne survivrait pas à la panne qu'il anticipe. Dans un fichier qui s'ouvre sur « les cartes SD finissent toujours par lâcher ».
- Pas de piège de focus sur les feuilles modales. C'est la seule lacune structurelle d'accessibilité du front, et elle est identifiée.
- Deux secrets restent en clair sur la partition de démarrage. Structurellement inévitable pour la configuration réseau, assumé pour un usage familial — pas acceptable dans un contexte professionnel.
Pourquoi ce projet est ici
Ce qu’il démontre
Personne ne l'a commandé, personne ne l'a spécifié, et il tourne tous les jours devant des utilisateurs qui n'ont aucune indulgence.
Il couvre toute la chaîne : conception produit, schéma de données, algorithmes sous contraintes, temps réel, IA appliquée avec sa frontière tenue par du code, authentification sans mot de passe, biométrie, application installable et hors ligne, provisionnement de machine, intégration continue, déploiement avec retour arrière vérifié. Sur une machine à quarante euros, avec sept dépendances.
Stack
Application
- Next.js 15
- React 19
- Tailwind CSS 4
- Server Actions
- TypeScript strict
Données & temps réel
- SQLite (module natif Node)
- Server-Sent Events
- Bus d’évènements
- Transactions imbriquées
Sécurité
- WebAuthn / biométrie
- Preuve de travail
- Sessions hachées
- Limitation glissante
Client
- PWA
- Service Worker
- Mode hors ligne
- visualViewport
- Accessibilité
Exploitation
- Raspberry Pi OS Lite
- systemd
- GitHub Actions
- Tunnel sortant
- Python (collecte)