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-05 · Statut : à jour
closers), un seul closer est exposé au démarrage (slug matt). En ajouter d'autres = insérer une ligne closers + ses availability/questions, chacun a sa page book.panelbay.com/<slug>.book.panelbay.com/<slug>, défaut /matt). Back-office : https://book.panelbay.com/admin (basic auth nginx)./root/workspace/apps/closerlib/google_workspace.py (googleapiclient, déjà installé).closer.service (sudo systemctl restart closer.service)book.panelbay.com → 127.0.0.1:8353. /admin et /api/admin protégés par basic auth (/etc/nginx/.closer_htpasswd, user matt), le reste public.sudo systemctl restart closer.service. Backend (backend/*.py) => restart requis. Frontend (frontend/*.html) => servi no-cache, PAS de restart (recharger la page).backend/app.py — serveur HTTP. Endpoints publics + back-office (voir §5). Port 8353.backend/db.py — schéma SQLite + seed_demo() (closer matt, horaires lun-ven 9-12/14-18, 3 questions). Tables : closers, availability, questions, leads, bookings, reminders.backend/slots.py — génère les créneaux réservables = horaires hebdo − (délai mini + horizon) − occupations Google Calendar − RDV déjà pris. Tout calculé en heure locale du closer, renvoyé en UTC.backend/gcal.py — Google Calendar via lib/google_workspace (scope calendar.events). busy_intervals() lit les événements occupant l'agenda (pas freebusy) ; create_event()/delete_event() pour le RDV. Best-effort : si Calendar tombe, le booking se fait quand même sans event.backend/mailer.py — envoi email via API Gmail (scope gmail.send). Envoie DEPUIS le compte Google du closer (son adresse en expéditeur).backend/whatsapp.py — canal WhatsApp/SMS, prêt mais inactif tant qu'aucun fournisseur n'est configuré. Supporte Meta WhatsApp Cloud API (WHATSAPP_PHONE_NUMBER_ID+WHATSAPP_TOKEN) ou Twilio (TWILIO_ACCOUNT_SID+TWILIO_AUTH_TOKEN+TWILIO_WHATSAPP_FROM). is_configured() = False sinon.backend/reminders.py — planifie (à la réservation) et envoie confirmation + rappels 24h/1h. Email toujours ; WhatsApp seulement si configuré ET lead a un téléphone.backend/reminders_cron.py — appelé par cron toutes les 10 min, envoie les rappels dus (idempotent).backend/_cleanup_testdata.py — utilitaire ponctuel : purge leads/bookings/reminders de test + supprime les events agenda liés. Ne touche pas closers/horaires/questions.backend/data/closer.db — base SQLite (WAL).frontend/index.html — widget public de booking, multi-étapes (1 contact, 2 qualification, 3 créneau). Capture progressive : POST /api/lead dès l'étape 1 (et au blur de l'email si nom+email valides), lead_id gardé en localStorage. Servi no-cache.frontend/admin.html — back-office : KPI (leads capturés, en cours, abandonnés, RDV pris, taux réservation, taux présence), table des leads filtrable (statut + recherche), actions (honoré/no-show/annuler + notes), export CSV, onglet Réglages (closer, horaires par jour éditables, questions, compte Google).status brut = incomplete | booked | canceled | completed | noshow. Le back-office calcule un statut d'affichage : booked→in_progress si <20 min et pas de RDV, sinon abandoned ; un lead avec booking confirmé = booked (ou completed/noshow si marqué). last_step (1/2/3) = jusqu'où il est allé.lead_id (uuid renvoyé au client). On ne régresse jamais last_step, on n'écrase jamais un lead déjà booked.availability (horaires hebdo) + exclusions Calendar/bookings. Le closer édite ses horaires dans le back-office (onglet Réglages).google_account (un des comptes connectés : matt.bouthors, moapropertiespy, moarealestate.sa, resident.paraguay). calendar_id défaut primary. L'event du RDV est créé sur cet agenda, l'email de confirmation part de cette adresse.Public :
GET / → redirige vers le closer par défaut. GET /<slug> → page de booking.GET /api/config?slug= → config publique du closer + questions.POST /api/lead → upsert progressif. Body {slug, lead_id?, name, email, phone, step, answers, utm, source}. Renvoie {lead_id}.GET /api/slots?slug=&from=&days= → {days:{date:[{start_utc,label}]}, timezone, duration_min}.POST /api/book → {lead_id, start_utc, hp?}. Re-valide le créneau, INSÈRE le booking (unicité garantie par l'index UNIQUE partiel uniq_book_slot, IntegrityError → 409), crée l'event agenda (best-effort, après l'insert), planifie + envoie la confirmation. Renvoie {ok, email_sent, when_label} ou 409 créneau_indisponible. email_sent reflète l'envoi réel de la confirmation (le widget adapte son message).GET /health → ping DB, {ok:true} (200) ou {ok:false} (500). Public, pour supervision./api/lead 12/min, /api/book 6/h), délai mini 3s entre deux nouveaux leads, honeypot (hp/website) → faux succès sans stockage. IP prise sur X-Real-IP (ou dernière entrée X-Forwarded-For), non spoofable.Back-office (basic auth nginx + garde applicative X-Admin-Token) :
GET /admin → back-office. GET /api/admin/leads?status=&closer=&q= → leads (statut d'affichage calculé).GET /api/admin/stats → compteurs + show_rate + book_rate + abandons par étape.POST /api/admin/lead → {id, status?, notes?}.GET /api/admin/settings / POST /api/admin/settings → lire / écrire closer + horaires + questions.GET /api/admin/export.csv → export CSV des leads.lib/google_workspace.py, scopes calendar.events + gmail.send déjà accordés). Pas de secret propre à l'app..env (voir whatsapp.py). Meta Cloud API recommandé (l'AIOS a déjà une app Meta). Tant que non configuré, seul l'email part.ADMIN_TOKEN (/root/workspace/apps/closer/.env) : garde applicative du back-office (défense en profondeur, en plus de la basic auth nginx). L'app exige X-Admin-Token sur /admin et /api/admin/* quand le token est défini. nginx doit injecter cet en-tête (proxy_set_header X-Admin-Token "<valeur>";) sur les deux location admin — à faire par l'orchestrateur. Tant que ce n'est pas fait, le repli transitoire accepte le trafic portant l'Authorization de la basic auth nginx (donc le back-office continue de marcher). Une fois nginx à jour, passer ADMIN_TOKEN_STRICT=1 pour retirer ce repli. Rotation suivie dans Audit-apps/_secret_rotation_closer.tsv./etc/nginx/.closer_htpasswd (user matt). Regénérer : python3 -c "import crypt;open('/etc/nginx/.closer_htpasswd','w').write('matt:'+crypt.crypt('NOUVEAU_MDP',crypt.mksalt(crypt.METHOD_SHA512))+chr(10))" puis sudo systemctl reload nginx.*/10 * * * * → reminders_cron.py (envoie confirmations/rappels dus, log backend/data/reminders.log).data/reminders.log et les logs du service en cas de doute.booked : rien n'est perdu.ORDER BY id LIMIT 1). Pour gérer plusieurs closers finement, il faudra étendre l'admin (sélecteur de closer). Le public, lui, gère déjà N closers par slug.ss -ltnp | grep 8353 puis kill le PID.data/closer.db.bak-*). (1) Double-booking : index UNIQUE partiel uniq_book_slot sur bookings(closer_id,start_utc) WHERE status='confirmed' (migration idempotente db.migrate(), dédoublonnage non destructif si besoin) ; _post_book insère d'abord et attrape IntegrityError → 409 (l'unicité est garantie par la base, plus par une lecture-puis-écriture) ; l'event agenda est créé APRÈS l'insert réussi (plus d'event orphelin en cas de course). (2) Anti-spam : rate-limit par IP en mémoire (/api/lead 12/min, /api/book 6/h), délai mini 3s entre nouveaux leads, honeypot caché (hp/website) → faux succès sans stockage. (3) Validation serveur : regex email + rejet CR/LF/NUL (bloque l'injection d'en-tête avant l'envoi Gmail, garde aussi dans mailer.send), bornes téléphone (7–20 chiffres) et answers (30 clés max, valeurs bornées). (4) Auth applicative admin : garde X-Admin-Token (env ADMIN_TOKEN) sur /admin + /api/admin/*, repli transitoire sur la basic auth nginx tant que l'en-tête n'est pas injecté (ADMIN_TOKEN_STRICT=1 pour le retirer). (5) Injection formule CSV : cellules commençant par = + - @ (ou tab/CR) préfixées d'une apostrophe dans l'export. (7) email_sent renvoyé par /api/book, message de succès du widget adapté. (9) GET /health (ping DB) + logs _log() des échecs booking/mail. (11) _client_ip prend X-Real-IP (sinon dernière entrée XFF), non spoofable. Accessibilité : jours/créneaux en vrais <button> (aria-pressed, focus visible), label for/id, .fielderr en role=alert aria-live. CLS : #app hauteur réservée + réhydratation locale de la saisie (nom/email/tel/réponses) au reload. Reporté : #8 annulation self-service (token signé), #10 i18n ES/EN. À faire orchestrateur (nginx) : sur location /admin et location /api/admin, ajouter proxy_set_header X-Admin-Token "<ADMIN_TOKEN>"; (valeur dans apps/closer/.env), un limit_req anti-flood, et un client (garder la basic auth). Tests OK : /health 200, admin 401 sans jeton / 200 avec X-Admin-Token ou Authorization, email malformé (CRLF) → 400, honeypot → non stocké, book bogus → 409, service active.db.py (6 tables, multi-closer), slots.py (créneaux = horaires − occupations Calendar − bookings), gcal.py (events.list pour les occupations + insert du RDV, scope calendar.events), mailer.py (Gmail API, scope gmail.send), whatsapp.py (Meta Cloud API / Twilio, inactif tant que non configuré), reminders.py + reminders_cron.py (confirmation + rappels 24h/1h, cron */10). Frontend : widget public multi-étapes (capture dès l'étape contact, upsert par lead_id en localStorage) + back-office (KPI, table filtrable, actions honoré/no-show/annuler + notes, export CSV, réglages horaires/questions éditables). Service closer.service port 8353. Exposé sur https://book.panelbay.com (Cloudflare + Let's Encrypt), /admin en basic auth. Ajouté au portail (Pilotage). Vérifié : chaîne API complète (capture progressive étape 1→2, créneaux, booking + event agenda + email de confirmation envoyé, stats) + Playwright (widget 3 étapes, back-office, réglages) + auth nginx (public 200, /admin 401 sans auth / 200 avec). Devise/agenda par défaut = compte matt.bouthors, fuseau America/Asuncion. En attente : fournisseur WhatsApp (email opérationnel dès maintenant). Données de test purgées, event de test retiré de l'agenda.