10 KiB
Tofu
Planning de repas et bibliothèque de recettes pour un foyer.
L'écran sert à saisir et à calculer ; l'usage final est une feuille de papier imprimée accrochée dans la cuisine. C'est un outil discret : pas de compte, pas de notification, pas de tableau de bord. On ouvre, on écrit sept plats, on imprime, on referme.
Les plannings sont enregistrés dans votre Nextcloud, en WebDAV, depuis le navigateur. Il n'y a pas de serveur Tofu, parce qu'il n'y a pas de serveur Tofu.
Contraintes, et pourquoi
Tofu est un dossier de fichiers. Pas de build, pas de node_modules, pas de bundler, pas
de CDN au runtime. Ces contraintes ne sont pas un exercice de style : elles existent pour
qu'on puisse rouvrir ce dépôt dans dix ans et que le seul geste nécessaire soit de servir
le dossier.
| Contrainte | Raison |
|---|---|
| Aucune étape de build | Un npm run build dépend d'un outillage qui se périme plus vite que le code. Ici, le fichier qu'on lit est le fichier qu'on exécute. |
Preact via htm/preact, jamais de JSX |
Le JSX impose un compilateur. Le template tagué html`…` est du JavaScript standard qui tourne tel quel dans le navigateur. |
Libs versionnées dans libs/ |
Aucun accès réseau au chargement. Un CDN qui disparaît ou change de politique ne casse pas l'application. |
| Modules natifs + import map | Pas de résolution magique : ce que le navigateur voit, c'est ce qui est écrit. |
| Aucune dépendance de test | node --test est dans Node. Rien à installer, rien à mettre à jour. |
| Aucune police distante | Uniquement des piles système. Une police hébergée ailleurs est une panne future. |
| Pas de backend, pas de proxy applicatif | Un service de plus à déployer, maintenir et surveiller — et il verrait passer le mot de passe. Voir docs/webdav-nextcloud.md. |
Si quelque chose semble impossible sans build : le signaler, pas réintroduire un bundler.
Seule exception : scripts/import-recipes/, outil d'amorçage lancé une fois à la main,
hors produit. Node et npm y sont libres. L'application ne dépend jamais de ce script.
Servir l'application
python3 -m http.server 8000
Puis http://localhost:8000. N'importe quel serveur de fichiers statiques fait l'affaire ;
il n'y a rien à configurer. Ouvrir index.html directement par file:// ne fonctionne
pas : les modules ES et l'import map exigent le protocole http.
En production, un nginx ou un apache qui sert le dossier suffit. La façon la plus
robuste de déployer est de servir Tofu sur la même origine que le Nextcloud — voir
docs/webdav-nextcloud.md, §5.
Lancer les tests
npm test # node --test "tests/**/*.test.js"
Aucune dépendance, aucun npm install. Node 22 ou plus récent (le lanceur de tests de Node
interprète lui-même le motif tests/**/*.test.js).
Chaque fonction pure de app/tools/ a son fichier de test dans tests/, un par fonction.
Les composants et les services ne sont pas testés unitairement : ce qui les concerne se
vérifie dans un navigateur, et pour le WebDAV par le diagnostic intégré à l'écran de
connexion.
Structure
index.html # import map, feuilles de style, point de montage
package.json # aucune dépendance ; champ "imports" pour Node
docs/webdav-nextcloud.md
libs/ # preact 10.27.2, preact/hooks, htm 3.1.1 — figés
styles/ # tokens.css, base.css, sheet.css, print.css
app/
main.js App.js
components/atoms/ # Icon, TextField, StatusLine, SuggestionList
components/molecules/ # DishNameField, DayEntry, WeekNavigation, ConnectionForm, WebdavCheckReport
components/pages/ # PlanningPage, ConnectionPage
hooks/ # useWeekPlanning, useRecipeIndex
services/webdav/ # LE seul endroit du projet qui fait un fetch
services/diagnostics/ # runWebdavCheck
services/planning/ # store Nextcloud + store mémoire, même contrat
services/recipes/ # source de recettes
services/scheduling/ # createSaveScheduler
const/ # constantes objets, en MAJUSCULES
tools/ # une fonction pure par fichier
tests/ # un test par fonction pure
scripts/import-recipes/ # hors produit : Node + npm autorisés ici seulement
Résolution des modules
Tout import interne s'écrit #tofu/…, et s'écrit pareil dans le navigateur et dans
Node :
import { normalizeSearchText } from '#tofu/tools/normalizeSearchText.js'
import { html } from 'htm/preact'
Le navigateur le résout par l'import map d'index.html, Node par le champ imports de
package.json. Ni ../.., ni chemin absolu /app/…, ni URL : un fichier déplacé ne
casse rien, et un test Node importe exactement ce que le navigateur charge.
Régénérer libs/
Le dossier est figé et versionné ; il n'y a normalement rien à y faire. Pour monter une version, les trois URLs exactes qui reproduisent les fichiers actuels à l'octet près :
curl -o libs/preact.js 'https://esm.sh/preact@10.27.2/es2022/preact.bundle.mjs'
curl -o libs/preact-hooks.js 'https://esm.sh/preact@10.27.2/X-ZXByZWFjdA/es2022/hooks.bundle.mjs'
curl -o libs/htm-preact.js 'https://esm.sh/*htm@3.1.1/es2022/preact.bundle.mjs'
Trois détails comptent, et ce sont eux qui empêchent un accès CDN au runtime :
.bundle.mjs, et non le raccourcihttps://esm.sh/preact: le raccourci renvoie une seule ligne,export * from "/preact@10.27.2/…", c'est-à-dire un ré-import depuis le CDN au chargement de la page. Exactement ce qu'on refuse.- Le
*devanthtm@3.1.1marque toutes les dépendances comme externes. Le fichier obtenu contientimport { h } from "preact"— un nom nu, résolu par l'import map verslibs/preact.js. Sans le*, htm importerait Preact depuis une URL esm.sh. - Le segment
X-ZXByZWFjdAjoue le même rôle pourpreact/hooks(il encodeexternal=preact). On l'obtient en suivanthttps://esm.sh/preact@10.27.2/hooks?target=es2022&external=preact, qui renvoie le chemin profond correspondant.
Vérification après téléchargement — la sortie ne doit contenir que des noms nus, aucune URL :
grep -o 'from"[^"]*"' libs/*.js
libs/README.md détaille ce choix.
État des trois tâches
Tâche 1 — parler au Nextcloud en WebDAV depuis le navigateur.
app/services/webdav/ est la seule brique du projet qui fait un fetch, et elle ne jette
jamais : tout échec devient un objet { ok: false, failure } avec un diagnostic en
français. L'écran de connexion exécute runWebdavCheck — joindre, créer le dossier,
écrire, relire, lister, nettoyer — et affiche chaque étape. Ce qui reste à prouver : le
CORS, qui ne se vérifie que depuis un vrai navigateur contre un vrai serveur. Le sujet est
entièrement instruit dans docs/webdav-nextcloud.md, avec une recommandation classée ;
il n'est pas tranché tant que le diagnostic n'a pas tourné.
Tâche 2 — importer les recettes existantes.
scripts/import-recipes/ lit un dossier de fichiers .odt, en extrait le texte, demande
à un modèle une sortie structurée, et écrit un JSON schema.org par recette plus un index
global. Les lignes d'ingrédients sont conservées telles qu'écrites ; le découpage
tofu:parsedIngredients n'existe que pour la recherche et le futur cumul des courses, et
une imprécision reste imprécise (« une poignée » ne devient jamais 30 g). Ce qui reste à
faire : le lancer sur le vrai dossier de recettes.
Tâche 3 — la feuille de planning. Sept jours, un plat et une ligne d'ingrédients par jour, navigation de semaine en semaine, suggestions de plats tolérantes aux fautes de frappe (« knoci » propose « Gnocchis à la sauge »), enregistrement automatique différé, et une mise en page pensée pour l'impression. Choisir une suggestion remplit la ligne d'ingrédients seulement si elle est vide : rien de ce que l'utilisateur écrit n'est jamais écrasé.
Limites connues
- Le CORS n'est pas tranché. Nextcloud ne renvoie aucun en-tête CORS sur
/remote.php/dav/et répond 401 au préflight (mesuré, voirdocs/webdav-nextcloud.md). Trois options existent, classées dans ce document ; aucune n'est validée tant que le diagnostic de l'écran de connexion n'a pas tourné sur le serveur réel. Depuis le navigateur, un blocage CORS est indiscernable d'une panne réseau — Tofu le dit explicitement plutôt que d'afficher « erreur réseau ». - Le mot de passe n'est pas conservé — c'est un choix. Ni
localStorage, ni cookie, ni session : il disparaît au rechargement de la page, et il faut le ressaisir. Stocker un identifiant Nextcloud dans un stockage navigateur accessible en JavaScript n'est pas un compromis qu'on veut faire pour éviter une ressaisie hebdomadaire. Utiliser un mot de passe d'application (Paramètres → Sécurité), révocable indépendamment du compte. - Les suggestions viennent d'une fixture. Tant que
scripts/import-recipes/n'a pas tourné, l'index de recettes estapp/const/RECIPE_INDEX_FIXTURE.js: une douzaine de recettes plausibles, écrites à la main. La recherche fonctionne, le corpus est faux. L'index produit par l'import a exactement la même forme : il suffira de le substituer. - Sans Nextcloud, rien n'est enregistré. « Continuer sans Nextcloud » fait tourner le planning en mémoire. La feuille reste saisissable et imprimable, l'état affiché le dit en toutes lettres (« Hors ligne — rien n'est enregistré »), et tout est perdu au rechargement. C'est un mode annoncé, pas une panne.
- Pas de mode sombre, et ce n'est pas un oubli. Tofu imite une feuille de papier posée
sur un bureau, et une feuille de papier est blanche.
color-scheme: lightest déclaré explicitement pour que le navigateur n'inverse rien. Ajouter un thème sombre reviendrait à maintenir deux palettes pour un écran qu'on regarde trois minutes avant d'imprimer. - Pas de liste de courses, pas de cumul des quantités. Le script d'import prépare
déjà les données nécessaires (
tofu:parsedIngredients), l'application ne les exploite pas encore.