200 lines
10 KiB
Markdown
200 lines
10 KiB
Markdown
# 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
|
|
|
|
```sh
|
|
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
|
|
|
|
```sh
|
|
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 :
|
|
|
|
```js
|
|
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 :
|
|
|
|
```sh
|
|
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 raccourci `https://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 `*` devant `htm@3.1.1`** marque toutes les dépendances comme externes. Le fichier
|
|
obtenu contient `import { h } from "preact"` — un nom nu, résolu par l'import map vers
|
|
`libs/preact.js`. Sans le `*`, htm importerait Preact depuis une URL esm.sh.
|
|
- **Le segment `X-ZXByZWFjdA`** joue le même rôle pour `preact/hooks` (il encode
|
|
`external=preact`). On l'obtient en suivant
|
|
`https://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 :
|
|
|
|
```sh
|
|
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é, voir `docs/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 est `app/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: light` est 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.
|