← Tous les manuels · Portail
↗ Ouvrir l'app

MANUAL — moa-cleaning

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é

1. Identité

  • - Rôle : formulaire de checklist de ménage pour les femmes de ménage de la conciergerie MOA Properties. Elles ouvrent un lien (sans compte) depuis Telegram, cochent la checklist du check-out pièce par pièce, ajoutent des photos, et envoient. L'admin (Matt) consulte les soumissions.
  • - URL publique : https://cleaning.panelbay.com
  • - Entité : MP (MOA Properties)
  • 2. Exécution / infra

  • - Dossier : /root/workspace/apps/moa-cleaning
  • - Stack : Node.js / Express + better-sqlite3 + frontend vanilla (SPA)
  • - Service systemd : moa-cleaning.service (systemctl restart moa-cleaning.service)
  • - Port : 8311 (bind 127.0.0.1)
  • - Reverse proxy (nginx) : cleaning.panelbay.com
  • - Redémarrer : systemctl restart moa-cleaning.service (nécessaire après tout changement backend)
  • 3. Structure & fichiers clés

  • - backend/server.js — API + service des pages
  • - backend/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.html
  • 4. Données

  • - Base / stockage : data/moa-cleaning.db (SQLite, WAL). Photos = fichiers sur disque sous data/uploads/<appart>/<date>/.
  • - Modèle / tables :
  • 5. API / endpoints

  • - 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.
  • 6. Intégrations & secrets

    n8n — scénario "envoi soir"

    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.

    Cerveau MOA

  • - Onglet data : les réservations (lu par n8n en étape 2).
  • - Onglet menage_config (nom_court/codigo/capacidad) : source prévue pour ajouter un appart (branchement app ↔ menage_config pas encore fait, voir CHANGELOG / TODO).
  • Secrets / clés (emplacements, jamais les valeurs)

  • - data/admin_key.txt — clé du dashboard admin
  • - data/prepare_key.txt — clé dédiée n8n pour /api/prepare
  • - Telegram / Google Sheets : les credentials vivent dans n8n (pas dans cette app).
  • 7. Pièges connus / à savoir

  • - Cerveau MOA = READ-ONLY (règle stricte) : l'app ne fait que LIRE le Sheet, jamais écrire. L'onglet menage_config a été créé une fois avec autorisation explicite de Matt.
  • - Après tout changement backend : redémarrer le service (le process Node fige son code au démarrage).
  • - Le lien par appart est fixe (token), il ne doit pas changer : il est collé dans les briefings Telegram.
  • - Suivi périodique par point (depuis 2026-08-04) : 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é.
  • - Suppression de lignes en base : le garde-fou bash bloque DELETE FROM. Passer par un script .js/.py (fichier), pas une commande bash inline.
  • 8. CHANGELOG

  • - 2026-08-04 — Suivi périodique par point (demande Matt). (1) Le trimestriel (et toutes les occasionnelles) se valide point par point, plus tout d'un bloc. (2) Remplissage partiel autorisé : la femme de ménage coche seulement ce qu'elle a fait. (3) Les points non faits gardent la bonne date et reviennent au ménage suivant. Impl : nouvelle table 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).
  • - 2026-08-02 — Stockage de l'export n8n complet dans integrations/n8n-envoi-soir.json + décomposition nœud par nœud dans la section Intégrations (manuel hyper-complet).
  • - 2026-08-02 — Ajout 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é.
  • - 2026-08-02 — Manuel initialisé (bootstrap auto).
  • CHANGELOG

  • - 2026-08-11 : Photos du formulaire : fiabilisation de la capture (elles ne « disparaissent » plus). Remontée terrain : après avoir pris une photo dans le formulaire, elle disparaissait. Causes diagnostiquées : (a) 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.
  • - 2026-08-07 : Quantités d'inventaire (demande femme de ménage). Problème : des items d'inventaire étaient cochables mais sans possibilité de saisir une quantité (couverts, perchas…). Solution : support générique 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.
  • - 2026-08-05 : corrections d'audit (P1/P2 + a11y). Appliquées dans le dossier de l'app :
  • - 2026-08-04 : migration vers panelbay.com. Nouvelle URL https://cleaning.panelbay.com (HTTPS Cloudflare + Let's Encrypt). L'ancienne https://menage.bouthors-m.tenga.run reste active en parallele (double-service) le temps de la transition.
  • - 2026-08-25 : service tombé (arrêt propre en masse à 18h37 le 24/08, non rattrapé par Restart=on-failure). Relancé + policy passée à Restart=always pour éviter la rechute.