Manuel d'instructions de cette app. À LIRE avant toute intervention, et à METTRE À JOUR après chaque modification (ajouter une ligne au CHANGELOG en bas).
Dernière génération/MAJ : 2026-08-02 · Statut : relu et complété
/root/workspace/apps/moa-cleaningmoa-cleaning.service (systemctl restart moa-cleaning.service)systemctl restart moa-cleaning.service (nécessaire après tout changement backend)backend/server.js — API + service des pagesbackend/lib/checklist.js — définition de la checklist (DAILY + OCCASIONAL), dueOccasional() (legacy, bloc) et dueOccasionalItems() (par point, utilisé partout depuis 2026-08-04)backend/db/schema.sql — schéma SQLite (apartments, submissions, photos, pending_cleanings)backend/db/index.js — init DB + seed des 7 apparts (tokens fixes)frontend/index.html, frontend/js/form.js, frontend/js/admin.js, frontend/admin.htmldata/moa-cleaning.db (SQLite, WAL). Photos = fichiers sur disque sous data/uploads/<appart>/<date>/.apartments : les 7 apparts, token fixe par appart, + colonnes last_weekly/last_monthly/last_quarterly/last_ac. Depuis 2026-08-04 ces colonnes ne sont plus la source de vérité : elles sont un simple résumé pour l'admin (= date où le bloc a été entièrement fait), recalculées à partir de occasional_items.occasional_items : source de vérité du suivi périodique, un point à la fois (apartment_id, block_key, item_index, last_done). Un point est « dû » si last_done est nul ou plus vieux que la période du bloc (7/30/90/120 j). La soumission ne date QUE les points réellement cochés ; les points non faits restent dus et reviennent au ménage suivant.submissions : 1 ligne par formulaire rempli (cleaner_name, prepare_for, answers JSON, occasional_done JSON, general_note, created_at).photos : photos rattachées à une submission (chemin disque).pending_cleanings : formulaire "armé" par n8n la veille (occasionnelles figées + prepare_for + next_guest), consommé à la soumission.GET /f/:token — page SPA du formulaire (lien fixe par appart)GET /api/form/:token — définition du formulaire (daily + occasionnelles dues/figées, prefill)POST /api/form/:token/photo — upload photo. Valide le mime réel (jpeg|png|webp), force l'extension depuis le mime, plafonne à 60 photos non soumises par appart, rate-limit 60/10 min par token. Réponse = {id} seulement (plus d'URL publique).POST /api/form/:token/submit — soumission (reset des last_done, consomme le pending). Rate-limit 20/10 min par token.POST /api/prepare — n8n arme un ménage (fige les occasionnelles dues). Auth X-Prepare-Key. Idempotent : dédup par (apartment_id, service_date), un retry n8n ne crée plus de doublon.GET /health — ping DB + submissions_total (observabilité).GET /uploads/* — service des photos gardé par la clé admin (X-Admin-Key ou ?key=). Les photos ne sont plus servies publiquement.GET /admin, GET /api/admin/apartments, GET /api/admin/submissions — dashboard admin (auth X-Admin-Key en header de préférence ; ?key= toléré pour les <img> de photos). La clé admin n'est plus imprimée au boot.Export complet du workflow (source de vérité) : integrations/n8n-envoi-soir.json (à réimporter dans n8n). Passation pour l'admin n8n (modifs à appliquer) : PASSATION-n8n.md.
Rôle : la veille au soir, n8n prépare et envoie le briefing de ménage du lendemain. Enchaînement des 8 nœuds :
1. envoi soir (Schedule Trigger) — déclenche chaque soir.
2. planning MOA (Google Sheets read) — lit l'onglet data du Cerveau MOA (1lZl0H0t3K41FvHmh1A_WCGpDyAA8owFl8D5hM7XpGyo), les réservations.
3. creation message (Code JS) — repère les départs (Sortie) de demain, cherche une arrivée le même jour pour le "preparer pour", et construit le message. Contient en dur, par appart : codesEntree (code d'entrée) et capacitesNormales (capacité). C'est ici qu'on ajoute le lien fixe du formulaire (voir PASSATION).
4. envoi MOA (Telegram, chat 5091962559) — message de supervision (compte "Elemail").
5. envoi suzana (Telegram, chat 8686096180) — même message à la femme de ménage (compte "telegram suzana").
6. Wait — attend 14h.
7. Code in JavaScript — re-parse le message envoyé pour ressortir ID_Match + preparer_pour par appart.
8. Update nbr voyageurs (Google Sheets update) — réécrit la colonne preparer pour dans le Cerveau MOA. ⚠️ C'est n8n (credential Google propre à n8n) qui écrit ici, jamais l'AIOS (règle Cerveau MOA read-only côté AIOS).
Modif prévue (voir PASSATION-n8n.md) : brancher chaque appart sur POST /api/prepare (armer le formulaire) + insérer le lien fixe du formulaire dans le message. Clé X-Prepare-Key.
data : les réservations (lu par n8n en étape 2).menage_config (nom_court/codigo/capacidad) : source prévue pour ajouter un appart (branchement app ↔ menage_config pas encore fait, voir CHANGELOG / TODO).data/admin_key.txt — clé du dashboard admindata/prepare_key.txt — clé dédiée n8n pour /api/preparemenage_config a été créé une fois avec autorisation explicite de Matt.dueOccasionalItems(dateMap, now, onlyKeys?) renvoie, par bloc, uniquement les points dus (nul ou écart ≥ période : hebdo 7j / mensuel 30j / trimestriel 90j / clim 120j). Le formulaire coche point par point ; le submit envoie occasional_done = objet { block_key: [item_index,…] } et ne date que ces points-là. Les points non cochés gardent leur date → re-proposés. dueOccasional() (niveau bloc) reste exporté mais n'est plus utilisé.DELETE FROM. Passer par un script .js/.py (fichier), pas une commande bash inline.occasional_items (date par point) + dueOccasionalItems() + submit occasional_done en objet {key:[idx]} + colonnes last_* reléguées à un résumé admin recalculé. Frontend : cases par point (occSection), compteur x/n. Admin : badges Bloc n. Migration idempotente (seed depuis last_*). Testé (submit partiel → points non faits re-proposés).integrations/n8n-envoi-soir.json + décomposition nœud par nœud dans la section Intégrations (manuel hyper-complet).POST /api/prepare + table pending_cleanings + clé prepare_key.txt (n8n arme le formulaire la veille, fige les occasionnelles). Prefill prepare_for/pending_id côté frontend. Reset des données de test (soumissions + dates last_*). Manuel complété.compressImage renvoyait le fichier original si la conversion échouait ; sur iPhone une photo HEIC part alors telle quelle et le backend (qui n'accepte que JPEG/PNG/WEBP via magic bytes) la rejette (415), le front retirait alors la vignette → « disparition » + message d'erreur ; (b) en cas d'échec (réseau/format) la vignette était supprimée au lieu d'offrir un re-essai ; (c) après un rechargement de la page, les vignettes n'étaient pas reconstruites (juste un texte « X fotos ya subidas »), ce qui donnait l'impression que les photos avaient disparu. Corrections (frontend, frontend/js/form.js + css/style.css) : (1) compressImage produit toujours un JPEG ; nouveau decodeImage() avec repli <img> (Safari sait rendre le HEIC en <img> même quand createImageBitmap refuse) → les photos iPhone se convertissent au lieu d'être rejetées ; si l'image est vraiment indécodable, on jette proprement (message clair) au lieu d'uploader un fichier voué au rejet. (2) Chaque photo devient une vignette persistante avec état visible : ⏳ (envoi) → ✓ vert (enregistrée) → ↻ rouge (échec, tap pour réessayer) ; une photo n'est jamais retirée en silence. (3) Après rechargement, on affiche une vignette ✓ « guardada » par photo déjà uploadée (les vraies images sont admin-guarded, donc placeholder) + message rassurant. (4) sw.js CACHE bumpé moa-cleaning-v2 → v3 (assets servis cache-first : sans bump, les téléphones gardaient l'ancien form.js). Aucun changement backend, pas de restart (front statique). Testé Playwright sur cleaning.panelbay.com (SW/cache purgés) : upload JPEG réel → vignette ✓ + photo en localStorage ; rechargement → vignette ✓ « guardada » persistante (capture validée). Photo de test (pending) supprimée (DB + disque). État data au 2026-08-11 : 2 soumissions reçues (Susana, 8 et 9 août) avec 14 et 15 photos attachées ; 17 photos « pending » (uploadées mais formulaire non envoyé, sessions antérieures). Le pipeline photo fonctionne donc déjà bout-en-bout ; ce correctif règle les cas d'échec (HEIC, réseau) et l'effet visuel de disparition.qty sur n'importe quel item de checklist. (1) lib/checklist.js : un item peut être une string (case à cocher) OU { text, qty:true } (champ nombre). Helpers itemText()/itemHasQty(). Nouvelle section daily "Inventario (conteo)" (icône 📦, 22 items comptables : tenedores, cuchillos, cucharas, cucharitas, platos llanos/hondos/postre, vasos, copas, tazas, ollas, sartenes, perchas, almohadas, juegos de sábanas, fundas, frazadas, toallas baño/mano, controles remotos, llaves/tarjetas, secador) chacun avec champ quantité, + intro. Le mensuel "Control y conteo de las perchas" devient aussi qty. dueOccasionalItems() renvoie désormais qty par point. (2) server.js : /api/form normalise les items daily en {text, qty} ; le submit accepte et stocke quantities ; l'admin les renvoie. (3) Migration idempotente : colonne submissions.quantities (JSON { itemId: {label, value} }, id = d:<sec>:<i> / o:<block>:<i>), ajoutée dans schema.sql + ALTER dans db/index.js. (4) Front form.js : items qty rendus en champ nombre (pas de case), state.quantities persisté en localStorage + envoyé au submit ; compteur x/n et answers dérivés (quantité saisie = fait). (5) admin.js : ligne "📦 Inventario : label valeur · …" par soumission + section ajoutée à la liste. (6) CSS .qty-input/.item.qty (font 16px = pas de zoom iOS). (7) SW bumpé moa-cleaning-v2 (sinon l'ancien front reste en cache PWA). Vérifié : node --check tous JS OK, service redémarré, colonne présente, /api/form expose la section + les flags qty, submit avec quantités stocké et relu, soumission de test supprimée + effet périodique annulé. Backup DB : data/moa-cleaning.db.bak-*-preInventory. Redémarrage du service requis fait. Note : quantités optionnelles (elle remplit ce qui s'applique). Pas de quantité "attendue" par appart (pas de source fiable) : saisie du réel uniquement ; on pourra brancher des valeurs attendues par appart plus tard (ex. via menage_config) si voulu.frontend/js/form.js) : tout l'état (nom, réponses, notes, occasionnelles, photos, note générale) est sauvegardé dans localStorage (clé cleaning_state_<token>) à chaque toggle et réhydraté au boot ; effacé au submit réussi. Un reload en plein ménage ne perd plus rien.backend/server.js) : suppression de express.static('/uploads'), remplacé par GET /uploads/* gardé par la clé admin (path-traversal bloqué). Le formulaire agent affiche un aperçu local (objectURL), il n'a plus besoin de lire la route.X-Admin-Key (fetch), ?key= conservé uniquement pour les <img> de photos ; l'admin scrube la clé de l'URL (history.replaceState) si elle arrive en query. Impression de la clé au boot supprimée (ne fuite plus dans journald).originalname), plafond 60 photos non soumises/appart, rate-limit maison (60/10 min sur /photo, 20/10 min sur /submit, par token)./api/prepare idempotent : dédup par (apartment_id, service_date) dans une transaction (les pendings antérieurs non consommés du même couple sont supprimés avant insert). Retry n8n = plus de doublon.form.js) : 404 → "Enlace inválido o expirado" (non retryable) ; 5xx/réseau → "Problema de conexión" + bouton Reintentar.GET /health (ping DB + total soumissions), erreurs submit/upload loguées (console.error), handler d'erreur JSON (multer inclus).frontend/manifest.webmanifest + frontend/sw.js (cache du shell, network-first sur la définition du formulaire pour un rendu offline, jamais de cache API-admin/photos) + icônes frontend/icons/icon-192|512.png. SW enregistré depuis form.js.<label> + vrai <input type=checkbox> (navigables au clavier, espace = toggle) + labels for/id sur les champs, focus visible (:focus-visible).--warn #f59e0b à --warn-d #b45309 (texte blanc conforme AA).node --check sur tous les JS, service redémarré, healthcheck 200, /health OK, gating /uploads (403 sans clé), admin header 403/200, upload non-image → 415, PNG réel → 201 (extension forcée .png), double /api/prepare → 1 seul pending. Sauvegarde DB avant intervention : data/moa-cleaning.db.bak-2026-08-05.client_max_body_size : ajouter client_max_body_size 20m; au bloc server 443 de cleaning.panelbay.com.conf (sous /etc/nginx/), sinon les photos > 1 Mo renvoient un 413 (multer autorise 15 Mo). Puis nginx -t && systemctl reload nginx.Environment=CLEANING_PUBLIC_URL=https://menage.bouthors-m.tenga.run dans /etc/systemd/system/moa-cleaning.service doit devenir https://cleaning.panelbay.com. Puis systemctl daemon-reload && systemctl restart moa-cleaning.service.