← Tous les manuels · Portail

MANUAL — closer

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

1. Identité

  • - Rôle : outil de prise de rendez-vous téléphoniques (alternative auto-hébergée à iClosed). Sa valeur centrale : capture progressive du lead. Le prospect est enregistré DÈS qu'il remplit l'étape contact (nom + email + téléphone), même s'il n'aboutit pas à une réservation ; on garde son contact et l'étape où il a abandonné. Un back-office liste tous les leads (en cours, abandonnés, RDV pris, honorés, no-show).
  • - Utilisateurs : côté public, les prospects qui réservent un appel. Côté interne, le(s) closer(s) et les dirigeants via le back-office.
  • - Multi-closer : le schéma est multi-closer dès le départ (table 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>.
  • - URL publique : https://book.panelbay.com (page de booking book.panelbay.com/<slug>, défaut /matt). Back-office : https://book.panelbay.com/admin (basic auth nginx).
  • - Entité : transverse (le closer choisit le compte Google = agenda + expéditeur, donc rattachable à MOA / RP / MP selon le besoin).
  • 2. Exécution / infra

  • - Dossier : /root/workspace/apps/closer
  • - Stack : Python (http.server, lib standard, zéro dépendance web) + SQLite + frontend statique (2 fichiers HTML vanilla JS). Accès Google via lib/google_workspace.py (googleapiclient, déjà installé).
  • - Service systemd : closer.service (sudo systemctl restart closer.service)
  • - Port : 8353 (127.0.0.1)
  • - Reverse proxy (nginx) : vhost 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.
  • - Redémarrer : sudo systemctl restart closer.service. Backend (backend/*.py) => restart requis. Frontend (frontend/*.html) => servi no-cache, PAS de restart (recharger la page).
  • 3. Structure & fichiers clés

  • - 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).
  • 4. Données / modèle

  • - Lead : 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é.
  • - Capture progressive : le lead existe dès l'étape 1. L'upsert se fait par lead_id (uuid renvoyé au client). On ne régresse jamais last_step, on n'écrase jamais un lead déjà booked.
  • - Créneaux : source de vérité = table availability (horaires hebdo) + exclusions Calendar/bookings. Le closer édite ses horaires dans le back-office (onglet Réglages).
  • - Agenda & email : par closer, champ 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.
  • 5. API / endpoints

    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.
  • - Anti-abus public : rate-limit par IP (/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.
  • 6. Intégrations & secrets

  • - Google Calendar + Gmail : credentials OAuth partagés de l'AIOS (lib/google_workspace.py, scopes calendar.events + gmail.send déjà accordés). Pas de secret propre à l'app.
  • - WhatsApp (optionnel, inactif par défaut) : à activer via .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.
  • - Basic auth back-office : /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.
  • - Cron : */10 * * * * → reminders_cron.py (envoie confirmations/rappels dus, log backend/data/reminders.log).
  • 7. Pièges connus / à savoir

  • - Rappels WhatsApp : planifiés seulement si un fournisseur est configuré ET que le lead a un téléphone. Sinon seuls les emails partent (les lignes whatsapp ne sont pas créées). Après avoir branché un fournisseur, ça s'active tout seul pour les NOUVEAUX bookings.
  • - Le rappel 24h est sauté si le RDV est réservé à moins de 24h (normal). Idem 1h si < 1h.
  • - Occupations agenda : on lit les événements de l'agenda du closer pour bloquer les créneaux, mais les événements journée entière ne bloquent pas (sinon un "OOO" toute la journée masquerait tout). Les événements marqués "disponible" (transparency) sont ignorés.
  • - Calendar/Gmail best-effort : si l'API Google échoue au moment du booking, le RDV est quand même enregistré en base (sans event, sans email) — vérifier data/reminders.log et les logs du service en cas de doute.
  • - Statut "abandonné" : purement calculé (incomplete + pas de RDV + >20 min). Un lead qui remplit l'étape 1 et revient 1h après pour finir repasse booked : rien n'est perdu.
  • - Multi-closer : tout le back-office actuel édite le PREMIER closer (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.
  • - Port 8353 : si le service refuse de démarrer avec "Address already in use", un process de test traîne : ss -ltnp | grep 8353 puis kill le PID.
  • 8. CHANGELOG

  • - 2026-08-05 — Corrections d'audit (P1 + P2, sécurité & robustesse). Backup DB avant migration (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.
  • - 2026-08-05 — Création de l'app (MVP). Booking RDV téléphoniques auto-hébergé avec capture progressive de leads (le cœur d'iClosed). Backend Python/SQLite : 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.