Espace projet de DoInLab
Technologie

L'architecture de DoInLab : FastAPI, SQLite et une IA remplaçable

Pourquoi SQLite n'est pas un défaut, pourquoi la configuration vit en base, comment l'IA reste interchangeable — et trois pièges rencontrés dont un formulaire HTML imbriqué.

DoInLab est une application web Python servie par un seul processus, sans chaîne de compilation front et sans dépendance externe au moment de l'exécution. Voici les choix qui la structurent — et ce qu'ils ont coûté ou rapporté.

La pile

  • FastAPI pour le routage, la validation et l'injection de dépendances ; Uvicorn comme serveur ;
  • Jinja2 pour le rendu — des pages HTML complètes, pas d'interface monopage ;
  • SQLAlchemy 2 sur SQLite, avec les clés étrangères activées explicitement ;
  • JWT en cookie httponly pour les sessions, mots de passe hachés avec bcrypt ;
  • Pillow pour le traitement des images, httpx pour les appels sortants, aiosmtplib pour la messagerie ;
  • aucune dépendance CDN : CSS et JavaScript sont locaux, ce qui permet un fonctionnement complet sur un réseau isolé.

Le code est découpé en modèles (utilisateurs, inventaire, projets, bibliographie, configuration, journaux), routes (une par domaine fonctionnel) et services (authentification, images, IA, e-mail, GitLab, configuration, journalisation, OIDC, prompts).

Tâches d'un projet DoInLab

L'interface est rendue côté serveur : des pages HTML complètes, des boutons de changement de statut, et pas une ligne de JavaScript compilé.

SQLite, et pourquoi ce n'est pas un défaut

Le choix surprend souvent. Il se défend : l'application vise un atelier, pas un site à fort trafic. SQLite supprime un service à administrer, une sauvegarde à orchestrer, une connexion à sécuriser — la base est un fichier dans un volume, qu'on copie pour la sauvegarder.

La limite est connue et assumée : les écritures sont sérialisées. Sur des dizaines d'utilisateurs simultanés qui consultent et éditent occasionnellement, c'est sans effet. Sur des centaines d'écritures concurrentes, il faudrait migrer — et l'usage de SQLAlchemy rend cette migration possible sans réécrire les requêtes.

La configuration en base plutôt qu'en fichiers

C'est une décision structurante : IA, messagerie, GitLab, authentification unique, paramètres d'application sont stockés dans une table clé/valeur, pas dans le fichier d'environnement.

Le bénéfice est immédiat en exploitation : un administrateur change l'URL du serveur d'IA depuis l'interface, sans accès au serveur et sans redémarrage. Seuls restent en variables d'environnement les paramètres nécessaires avant le démarrage : clé secrète, chemin de base de données, niveau de journalisation.

L'intégration IA : un seul contrat

Toutes les fonctions d'IA — descriptions d'équipement, analyse d'image, suggestions de projets, génération de tâches, assistant — passent par un service unique qui parle le protocole de l'API OpenAI. Conséquence : Ollama en local, LM Studio, un serveur d'inférence interne ou un service en ligne se configurent de la même façon, en changeant une URL et un nom de modèle.

Deux mécanismes méritent d'être détaillés.

Le streaming du chat

L'assistant utilise les événements envoyés par le serveur (SSE) : la réponse s'affiche au fil de la génération plutôt qu'après vingt secondes de silence. Côté serveur, c'est une réponse en flux ; côté navigateur, un composant JavaScript accumule les fragments et met à jour le DOM. Sur un modèle local, cette différence de perception est considérable.

L'analyse d'images

La photo est encodée en base64 et transmise dans un message multimodal. Le modèle configuré pour la vision doit évidemment savoir traiter des images — c'est un réglage distinct de celui du modèle de texte, précisément parce que tout le monde n'a pas les deux.

Les invites elles-mêmes sont stockées et modifiables depuis l'administration : adapter le ton ou le niveau de détail des descriptions ne demande pas de toucher au code.

Le traitement des images

Un atelier photographie beaucoup, et souvent avec un téléphone : des fichiers de plusieurs mégaoctets. L'optimisation se fait en deux passes :

  1. réduction progressive de la qualité, de 85 % à 40 %, en conservant les dimensions ;
  2. si le seuil d'un mégaoctet n'est toujours pas atteint, réduction des dimensions par paliers de 10 %.

Cet ordre n'est pas anodin : on préserve la lisibilité des sérigraphies — les inscriptions sur les composants — le plus longtemps possible, quitte à sacrifier la finesse de compression. Sur un inventaire de plusieurs centaines de photos, l'économie de stockage se compte en gigaoctets.

L'authentification, locale et déléguée

Le mode local repose sur un jeton JWT en cookie httponly avec l'attribut samesite, vérifié à chaque requête. Le mode délégué implémente OpenID Connect en flux d'autorisation, avec découverte automatique des points d'accès du fournisseur.

Deux choix d'implémentation valent d'être signalés :

  • l'état de l'échange — les jetons anti-rejeu — est conservé dans deux cookies à durée de vie courte plutôt que dans une session serveur. Pas d'état partagé, donc pas de contrainte de déploiement ;
  • la liaison de compte se fait à trois niveaux : par identifiant du fournisseur (liaison forte), à défaut par adresse e-mail (fusion), et enfin création automatique si l'administrateur l'autorise. C'est ce qui permet de basculer une base d'utilisateurs existante vers l'authentification unique sans casser les comptes.

Trois pièges rencontrés, et ce qu'ils enseignent

Des formulaires HTML imbriqués

Sur la fiche d'équipement, la suppression d'une photo était un formulaire… placé à l'intérieur du formulaire d'édition. Or la spécification HTML l'interdit : le navigateur ferme le formulaire parent, et l'édition ne se soumettait plus. Correction : des boutons ordinaires et un appel asynchrone. Un comportement inexplicable côté serveur a parfois sa cause dans le HTML.

Le champ vide qui n'est pas un entier

Des champs numériques facultatifs déclenchaient une erreur de validation : un navigateur envoie une chaîne vide pour un champ non rempli, pas une absence de valeur. Déclarer le champ comme entier facultatif ne suffit donc pas — il faut accepter la chaîne et convertir. Un classique, qui coûte toujours une demi-heure.

Une version de bibliothèque qui casse tout

Le passage de bcrypt en version 5 rendait la couche de hachage inopérante — un attribut interne supprimé, utilisé par la bibliothèque appelante. D'où une version épinglée dans les dépendances, et un commentaire expliquant pourquoi. Une version épinglée sans explication finit toujours par être « nettoyée » par quelqu'un.

La migration de schéma au démarrage

Le conteneur exécute un script de migration avant de lancer le serveur : il crée les tables manquantes sans jamais toucher aux existantes. Une mise à jour de l'image applique donc les évolutions de schéma toute seule.

C'est volontairement modeste — pas de migrations versionnées avec retour arrière — mais adapté au contexte : ajouts de colonnes et de tables, sur une base dont l'exploitant n'est pas administrateur de bases de données.

Ce qui n'a pas été fait, et pourquoi

  • Pas d'interface monopage — le rendu serveur suffit, et l'application se reprendra dans deux ans sans avoir à réparer d'abord son outillage de compilation.
  • Pas de dépendance CDN — pour fonctionner sur un réseau sans Internet.
  • Pas de file d'attente ni de cache distribué — l'usage ne le justifie pas, et chaque service ajouté est un service à exploiter.

Le fil conducteur est constant : préférer ce qui reste compréhensible et exploitable par une seule personne.

La série DoInLab

  1. DoInLab : la solution en un coup d'œil
  2. Le parcours fonctionnel, écran par écran
  3. L'architecture technique — vous y êtes
  4. Installer DoInLab avec Docker

Image publique : suntux57420/doinlab sur Docker Hub — hub.docker.com/r/suntux57420/doinlab.

Retour au blog