Aller au contenu

Mon Couple

Élément Valeur
Version candidate V1 (1.5.0)
Source de version apps/ovh-primary/moncouple/VERSION
Statut Retiré le 28 août 2026 ; runtime désactivé et redirection vers Notre Couple
Backend Python 3.14, bibliothèque standard
Frontend HTML/CSS/JavaScript sans framework
Données SQLite WAL sur PVC moncouple-data, conservé pour rollback

Retrait opérationnel — 28 août 2026

Mon Couple V1 n'est plus servi comme application active. Son code et son volume restent versionnés/conservés afin de permettre un rollback explicite sans supprimer les données historiques.

État GitOps attendu :

  • Deployment moncouple : 0 replica ;
  • Deployment moncouple-notifier : 0 replica ;
  • CronJob moncouple-reminder : suspend: true ;
  • PVC moncouple-data conservé, sans migration ni suppression de données dans ce retrait ;
  • moncouple.djayzone.com et moncouple.jemimah.fr conservent leur terminaison TLS mais répondent par une redirection HTTP permanente vers https://notrecouple.fr, en conservant le suffixe de chemin ;
  • Gatus surveille les deux redirections publiques et ne sonde plus le service interne volontairement arrêté.

Les sections fonctionnelles ci-dessous documentent la dernière version V1.5.0 conservée pour historique et rollback ; elles ne décrivent plus un runtime actif en production.

Politique de version

Mon Couple suit le versioning sémantique MAJEURE.MINEURE.CORRECTIF. Le badge public reste volontairement lisible (V1) tandis que GitOps, Kubernetes et la métadonnée HTML portent la version exacte.

La V1.1 ajoute une expérience complice en direct sans modifier le gameplay : présence respectueuse, attentions éphémères, célébrations visuelles, résumé de fin de session plus émotionnel, réactions décoratives locales, PWA, thème clair/sombre et écran de nouveautés.

La V1.1.1 enrichit le catalogue à 197 inspirations et impose une validation commune pour clôturer une session : Jason et Jemimah doivent tous les deux confirmer la fin (1/2 → 2/2).

La V1.2 synchronise les actions de jeu dans les deux navigateurs, introduit des tours de proposition équitables avec possibilité de passer et transforme le catalogue d'inspirations en interface compacte, filtrable et recherchable.

La V1.2.1 corrige le thème clair avec une palette réellement lumineuse et des contrastes dédiés pour la navigation, les formulaires, les cartes, les modales et les actions de vote, sans modifier le gameplay.

La V1.3 impose un consentement bilatéral aux amendements : leur auteur propose, l'autre profil accepte ou refuse, et seuls les amendements adoptés restent attachés à la proposition.

La V1.3 conserve le hotfix notifier V1.2.2 : le Deployment exécute directement notifier.py, la règle d'exclusion des sessions de révision vit dans ce runtime et aucun patch textuel de démarrage n'est utilisé.

La V1.4 Explorer à deux ajoute une couche d'exploration avant la décision : sessions thématiques, Déclic avec réponses secrètes, découverte surprise en session, catalogue d'inspirations qui se nettoie selon l'usage, sons discrets locaux et nouvelle navigation visuelle de Notre couple.

La V1.4.1 finalise cette expérience : les sessions thématiques ciblent par défaut l'Inventaire, les Inspirations et Déclic, Déclic accepte le swipe/drag gauche-droite, la découverte commune utilise le libellé personnel Ça me parle, Web Audio est réutilisé après déverrouillage navigateur, l'acceptation d'un amendement possède un son discret, la première bulle de constellation est correctement positionnée et le bouton desktop Déclic utilise la même iconographie SVG monochrome que le reste de la navigation.

La V1.4.2 est un hotfix frontend : l'entrée Session thématique est rendue directement dans le menu Démarrer une session, au même niveau que Session classique et Session de révision. Elle ne dépend plus uniquement d'une injection DOM post-rendu susceptible de manquer l'ouverture du menu.

La V1.5 transforme Jouer en rendez-vous quotidien : 160 activités réparties en 8 chapitres, 5 suggestions communes et stables par jour, jusqu'à 3 activités sélectionnées ensemble, réponses privées jusqu'au double envoi, apprentissages et Déclics bilatéraux, puis création facultative d'un souhait privé à partir d'une activité terminée.

Le cycle métier reste strictement identique : brouillon → inventaire privé → révélation/proposition → vote secret → négociation éventuelle → accord commun, avec les sessions de révision déjà existantes. Les fonctions V1.4.x peuvent alimenter ce cycle mais n'adoptent jamais automatiquement une inspiration.

Rôle

Mon Couple est l'ancienne application privée à deux profils destinée à construire un socle commun de souhaits et de règles. Depuis le 28 août 2026, Notre Couple constitue l'application active et Mon Couple est conservé uniquement pour historique/rollback.

L'application ne comporte ni score individuel, ni gagnant, ni streak, ni classement, ni compteur de concessions. Les fonctions de découverte ne transforment pas un intérêt individuel en pression sociale : un choix Déclic reste privé tant qu'il n'existe pas de correspondance positive des deux profils.

Accès et profils

Historiquement, deux noms DNS servaient le même runtime et le même volume SQLite. Ils sont désormais conservés uniquement comme points d'entrée de redirection vers Notre Couple :

  • https://moncouple.djayzone.com → https://notrecouple.fr ;
  • https://moncouple.jemimah.fr → https://notrecouple.fr.

La logique de profil par hostname reste dans le code V1.5.0 uniquement pour permettre un rollback. Tant que le retrait reste actif, aucune session Mon Couple n'est servie par ces domaines.

Les enregistrements DNS sont gérés manuellement dans Cloudflare et ne sont pas provisionnés par ce dépôt. Le retrait GitOps ne supprime donc pas les entrées DNS : elles doivent continuer à atteindre Traefik pour que la redirection et le TLS restent fonctionnels.

Authentification et confidentialité

L'application possède exactement deux comptes locaux. Les PIN sont hachés par PBKDF2-HMAC-SHA256. Les sessions sont stockées côté serveur dans SQLite et utilisent un cookie HttpOnly, Secure, SameSite=Strict avec expiration à 7 jours.

Les brouillons sont totalement privés : l'autre profil ne reçoit même pas leur existence. Pour un souhait privé non révélé, le backend ne transmet pas son titre ni sa description au navigateur adverse ; seules les métadonnées nécessaires à la carte mystère sont exposées.

Les choix Déclic (NO, MAYBE, YES) sont stockés par utilisateur dans SQLite mais ne sont jamais inclus dans l'état envoyé au partenaire. Un premier YES ne déclenche pas non plus de révision SSE partagée : le résultat n'apparaît dans Nos déclics qu'une fois les deux YES enregistrés.

Les protections HTTP comprennent notamment :

  • CSP restrictive sans dépendance frontend externe ;
  • X-Content-Type-Options: nosniff ;
  • X-Frame-Options: DENY ;
  • contrôle Host/Origin sur les mutations ;
  • cookies stricts ;
  • limitation des tentatives de connexion.

Architecture Kubernetes

État GitOps actuel :

  • Deployment moncouple : 0 replica web/API ;
  • Deployment moncouple-notifier : 0 replica ;
  • CronJob moncouple-reminder : suspendu ;
  • Ingress des deux anciens domaines conservé avec Middleware Traefik moncouple-to-notrecouple ;
  • PDB historique conservé ;
  • PVC moncouple-data, 1 GiB, local-path, ReadWriteOnce, conservé ;
  • SQLite WAL et configuration du runtime conservés pour rollback.

La configuration historique du web conserve une stratégie RollingUpdate, maxUnavailable: 0, maxSurge: 1, la readiness /healthz, minReadySeconds: 3, un preStop de 5 secondes et terminationGracePeriodSeconds: 20. Ces paramètres redeviennent pertinents uniquement si le runtime est réactivé.

Limite du stockage

Le PVC local-path reste lié au nœud qui porte le volume. Aucun writer Mon Couple n'est attendu tant que web et notifier restent à zéro. En cas de rollback, cette contrainte redevient applicable : plusieurs replicas web ne constituent pas une haute disponibilité inter-nœuds du stockage SQLite.

Cible produit grand public et HA

Cette section documente la cible qui avait conduit au développement de Notre Couple. L'implémentation Mon Couple à deux profils et SQLite ne constituait pas l'architecture cible d'un service grand public. Les principes étaient :

  • backend stateless, sans session ni état métier dépendant de la mémoire d'un pod ;
  • PostgreSQL externe ou opérée en haute disponibilité, avec sauvegardes, restauration testée, réplication et migrations versionnées ;
  • modèle multi-tenant explicite : chaque compte appartient à un couple ou foyer et toutes les requêtes sont isolées par cet identifiant ;
  • comptes individuels, invitations, récupération d'accès, révocation de session et consentement bilatéral sans identités codées en dur ;
  • contraintes, index et transactions compatibles avec plusieurs couples actifs simultanément ;
  • stockage objet pour les futurs médias, jamais sur le système de fichiers local d'un pod ;
  • observabilité, quotas, limitation d'abus et suppression/export des données conçus par tenant ;
  • déploiement réinstallable depuis zéro, configuration externalisée et absence de secrets dans Git.

Les tables SQLite historiques restent additives et le PVC est préservé ; aucune donnée Mon Couple n'est supprimée dans le retrait.

Assemblage du runtime

Le conteneur ne modifie jamais /app. Les fichiers générés sont écrits sous /tmp.

Séquence frontend :

  1. ui_patch.py : adaptations mobile, domaines/profils et socle compact ;
  2. branding_patch.py : favicon, badge V1 et métadonnée de version ;
  3. desktop_patch.py : couche graphique desktop ;
  4. revision_ui_patch.py : sessions classiques/révision ;
  5. revision_meta_patch.py : métadonnées d'adoption ;
  6. experience_ui_patch.py : expérience V1.1 live, PWA et thèmes ;
  7. session_consensus_ui_patch.py : clôture V1.1.1 validée à deux ;
  8. inspirations_ui_patch.py : catalogue compact, recherche et filtres V1.2 ;
  9. table_turn_ui_patch.py : alternance proposition/passe ;
  10. game_state_ui_patch.py : rafraîchissement SSE respectueux des formulaires ;
  11. release_ui_patch.py : écran cumulatif des nouveautés ;
  12. amendment_consent_ui_patch.py : décision bilatérale et affichage des amendements V1.3 ;
  13. explore_v14_ui_patch.py : sessions thématiques, Déclic, découverte surprise, sons et nouveau socle V1.4 ;
  14. polish_v141_ui_patch.py : ciblage thématique, swipe Déclic, iconographie navigation, audio et hotfix constellation V1.4.1 ;
  15. hotfix_v142_ui_patch.py : présence statique et déterministe de l'entrée Session thématique dans le split menu ;
  16. play_v15_ui_patch.py : hub Jouer quotidien, sélection, réponses et révélations V1.5 ;
  17. light_theme_patch.py : dernière couche visuelle dédiée au thème clair.

Séquence backend dans app_runner.py :

  1. fusion du catalogue d'inspirations ;
  2. revision_backend_patch.py ;
  3. revision_safety_patch.py ;
  4. live_backend_patch.py ;
  5. session_consensus_backend_patch.py ;
  6. table_turn_backend_patch.py ;
  7. amendment_consent_backend_patch.py ;
  8. game_state_backend_patch.py ;
  9. explore_v14_backend_patch.py ;
  10. play_v15_backend_patch.py : catalogue quotidien, confidentialité des réponses et passerelle vers les souhaits ;
  11. compilation/exécution de /tmp/app_runtime.py.

validate_runtime.py reconstruit et compile ce backend réellement généré. validate_frontend.py reconstruit toute l'interface dans le même ordre et vérifie également la syntaxe JavaScript lorsque Node est disponible.

V1.1 — présence en ligne respectueuse

Après authentification, l'interface peut afficher discrètement :

Jemimah est là ✨
Jason est là ✨

Fonctionnement :

  • heartbeat navigateur toutes les 17 secondes ;
  • présence considérée active pendant 45 secondes maximum après le dernier heartbeat ;
  • aucune heure exacte de dernière connexion n'est affichée ;
  • aucune table d'historique de présence ;
  • les lignes de présence expirées sont supprimées opportunément ;
  • l'état concerne uniquement le partenaire ;
  • rien n'est accessible avant authentification.

Apparaître hors ligne

Le contrôle 👁️ / 🙈 est mémorisé uniquement dans localStorage sur l'appareil.

Quand le mode invisible est actif :

  • aucun heartbeat n'est envoyé ;
  • le serveur ne reçoit donc pas de nouvelle information de présence ;
  • l'ancienne présence expire naturellement au plus tard 45 secondes après le dernier heartbeat.

Cette option ne modifie pas la session d'authentification et n'empêche pas l'utilisation normale de l'application.

V1.1 — attentions en direct

Une petite palette permet d'envoyer uniquement les huit attentions prédéfinies :

Type API Affichage
heart ❤️ Cœur
hug 🤗 Câlin
kiss 💋 Bisou
sparkles ✨ Étincelles
rose 🌹 Rose
wave 👋 Coucou
confetti 🎉 Confettis
thinking_of_you 💭 Je pense à toi

Aucun texte arbitraire n'est accepté. Le serveur refuse :

  • un type inconnu ;
  • un champ supplémentaire dans le payload ;
  • un corps live supérieur à la limite prévue ;
  • un envoi trop rapproché.

Le serveur déduit toujours l'émetteur depuis la session authentifiée et le destinataire comme l'autre utilisateur. Aucun sender_id ou recipient_id n'est accepté depuis le navigateur.

Limitation :

  • au moins 3 secondes entre deux attentions ;
  • maximum 10 attentions par minute et par utilisateur ;
  • événements expirés après 3 minutes.

Les événements live ne contiennent jamais de titre, description ou contenu de souhait. Le flux transmet uniquement le type d'attention et un identifiant technique croissant.

SSE et compatibilité avec deux replicas

Endpoints authentifiés :

POST /api/live/presence
GET  /api/live/stream
POST /api/live/attention

Une connexion SSE et l'envoi d'une attention peuvent atteindre deux pods différents. Pour cette raison, aucun bus d'événements Python en mémoire n'est utilisé.

SQLite sert de bus léger partagé :

live_presence
├── user_id       PK
└── last_seen_at

live_events
├── id             PK AUTOINCREMENT
├── sender_id
├── recipient_id
├── attention_type
├── created_at
└── expires_at

session_end_approvals
├── session_id
├── user_id
└── approved_at

Indexes :

idx_live_events_recipient_id(recipient_id, id)
idx_live_events_expires_at(expires_at)

Les migrations sont additives et idempotentes. Elles ne suppriment ni ne réécrivent les données métier existantes.

Flux SSE

Le serveur :

  • répond en text/event-stream ;
  • utilise Cache-Control: no-store ;
  • envoie X-Accel-Buffering: no ;
  • interroge SQLite au maximum environ une fois par seconde ;
  • envoie un commentaire keepalive toutes les 15 secondes ;
  • accepte Last-Event-ID ou un curseur explicite ;
  • diffuse aussi l'état du consensus de fin de session et la révision de gameplay partagée ;
  • ne marque jamais globalement un événement comme « livré » afin de rester compatible avec plusieurs onglets ;
  • nettoie opportunément les événements expirés.

Le navigateur conserve son dernier curseur localement et EventSource se reconnecte automatiquement après une coupure réseau ou un rollout.

Lors d'une toute nouvelle connexion sans curseur, le flux démarre après le dernier événement existant pour ne pas rejouer d'anciennes attentions.

Accessibilité des attentions et sons

Les animations sont non bloquantes et durent environ 2 à 3 secondes. Une région aria-live="polite" annonce leur arrivée sans dépendre uniquement de l'animation.

Avec prefers-reduced-motion: reduce, l'effet animé est supprimé au profit d'une présentation statique temporaire.

La V1.4 ajoute des sons facultatifs et discrets générés localement avec Web Audio pour certains événements relationnels. Ils sont désactivables et leur volume est stocké uniquement dans localStorage. La V1.4.1 réutilise un même AudioContext, tente de le reprendre après une interaction utilisateur et ajoute une signature légère lors de l'acceptation d'un amendement. Aucun fichier audio externe, musique de fond ou donnée de son n'est envoyé au serveur.

Améliorations visuelles V1.1

La V1.1 ajoute, sans effet métier :

  • célébration légère lorsqu'un nouvel accord rejoint le socle ;
  • écran de fin de session plus émotionnel construit uniquement à partir du résumé API existant ;
  • réactions décoratives sur un accord, stockées seulement dans localStorage sur l'appareil ;
  • avatars visuels par défaut pour les deux profils ;
  • messages d'accueil choisis dans une liste locale fixe ;
  • fenêtre de nouveautés affichée une seule fois par navigateur et par release ;
  • thème auto, clair ou sombre, mémorisé localement ;
  • installation PWA.

Aucune de ces fonctions n'ajoute un score, une récompense, une compétition ou une pression sociale.

PWA et mode hors ligne

Assets générés au démarrage sous /tmp :

manifest.webmanifest
sw.js
offline.html
icon-192.svg
icon-512.svg

Le service worker met uniquement en cache :

  • la page hors ligne ;
  • le manifest ;
  • les icônes locales.

Il ne met jamais en cache :

  • /api/* ;
  • le HTML authentifié de l'application ;
  • les réponses contenant des données privées.

Lorsqu'une navigation échoue hors ligne, une page statique explique simplement que les données restent côté serveur et qu'une reconnexion est nécessaire.

Sessions de jeu

Le split button Démarrer une session propose :

  • Session classique ;
  • Session de révision ;
  • Session thématique.

Depuis la V1.4.2, l'option Session thématique est incluse directement dans le markup du menu. Le garde-fou dynamique V1.4 reste présent mais détecte l'option existante et ne la duplique pas.

Session classique

Fonctionnement historique inchangé : nouveaux souhaits, révélations, votes secrets, négociations et accords adoptés.

La V1.4 ajoute sur cet écran 🎲 Fais-nous découvrir quelque chose. La carte tirée est commune et visible par les deux profils, sans devenir automatiquement un souhait. En session classique, le tirage peut être limité à une catégorie.

Session de révision

Une session de révision sert uniquement à reprendre des accords déjà adoptés.

  • une règle commune à la fois ;
  • aucun nouveau souhait posé ;
  • vote secret Conserver / Modifier / Retirer ;
  • toute demande de modification ou désaccord ouvre une négociation ;
  • nouvelle formulation puis nouveau vote ;
  • suppression logique par statut RETIRED, jamais destructive ;
  • adopted_at reste séparé de updated_at et retired_at.

Session thématique — V1.4/V1.4.1/V1.4.2

Une session thématique utilise le même moteur qu'une session classique mais lui associe 1 à 3 catégories. Les tours de proposition, votes, négociations, amendements et clôture à deux restent identiques.

Les catégories sont persistées dans game_sessions.theme_categories. Le frontend les affiche comme contexte de session et la découverte surprise ne tire que des inspirations appartenant à ces catégories. Le backend refuse une session sans thème, avec plus de trois thèmes ou avec une catégorie inconnue.

Depuis la V1.4.1, l'Inventaire, les Inspirations et les cartes Déclic sont filtrés par défaut sur ces catégories. Le profil peut sélectionner Voir tout localement pour accéder au reste de ses éléments ; ce choix ne modifie ni la session ni les données serveur. Lors de la création d'un nouveau souhait, les catégories du thème sont simplement placées en tête du sélecteur : aucune catégorie hors thème n'est interdite côté backend.

Clôture à deux — V1.1.1

Une session classique, thématique ou de révision ne passe plus à ENDED après un seul clic.

  • le premier profil valide la clôture ;
  • la session reste active et l'interface affiche En attente (1/2) ;
  • l'approbation est persistée dans session_end_approvals ;
  • le second profil valide à son tour ;
  • seulement à 2/2, la session est clôturée et le résumé final est affiché ;
  • une activité métier après le premier accord invalide l'approbation en attente afin que les deux validations portent sur le même état final ;
  • une règle encore active en session de révision bloque toujours la demande de clôture.

L'état 1/2 est exposé par /api/state et diffusé dans le SSE existant, ce qui reste cohérent lorsque les deux utilisateurs atteignent deux replicas différents.

Inspirations

Le catalogue contient 197 inspirations.

  • les 17 catégories disposent d'au moins 11 inspirations ;
  • inspirations-v1.1.1.json ajoute 102 idées générales, soit 6 par catégorie ;
  • inspirations-liberte-intime-v1.1.1.json ajoute 10 inspirations génériques supplémentaires dans Intimité autour de cadres consensuels de couple libre et de partage ;
  • la catégorie Intimité contient donc 21 inspirations ;
  • aucune préférence sexuelle personnelle des utilisateurs n'est versionnée dans Git.

Les inspirations sont importables en brouillon ou dans l'inventaire, puis deviennent des souhaits normaux éditables. Depuis la V1.4, un souhait importé conserve source_inspiration_id.

Le catalogue principal expose deux vues :

  • Disponibles : inspirations encore pertinentes pour le profil courant ;
  • Déjà explorées : inspirations masquées du catalogue principal.

Une inspiration importée dans l'inventaire (PRIVATE) disparaît uniquement pour son auteur. Un simple brouillon ne la masque pas. Le partenaire ne reçoit aucune information indiquant qu'une inspiration a été importée en privé.

Si un souhait relié à une inspiration est adopté sans modification du titre, de la description, de la catégorie et de l'importance, l'inspiration est considérée comme adoptée telle quelle et disparaît des disponibilités des deux profils. Une inspiration ayant servi de point de départ à un souhait fortement reformulé ne déclenche pas ce masquage global.

app_runner.py fusionne app.py, inspirations-extra.json, inspirations-v1.1.1.json et inspirations-liberte-intime-v1.1.1.json de manière idempotente.

V1.4 — Déclic

Déclic permet d'explorer le catalogue sans révéler une curiosité individuelle. Pour chaque inspiration, le profil choisit :

  • NO — Pas pour moi ;
  • MAYBE — Peut-être ;
  • YES — Ça me tente.

Les choix sont stockés dans inspiration_choices. /api/state ne transmet jamais le choix individuel du partenaire. Un premier YES ne provoque pas de notification, de mail ni d'événement SSE révélant le choix.

Lorsque les deux profils ont choisi YES, l'inspiration apparaît dans Nos déclics pour les deux. Elle peut ensuite être importée comme souhait mais n'est jamais automatiquement proposée ou adoptée.

La V1.4.1 ajoute le geste de swipe/drag : gauche = NO, droite = YES. Les trois boutons restent présents et les touches ←/→ offrent la même action lorsque la carte a le focus. MAYBE reste volontairement un choix explicite par bouton.

V1.4 — découverte surprise

POST /api/discovery/next choisit une inspiration commune durant une session classique ou thématique. L'état courant est stocké dans SQLite afin que les deux replicas et les deux navigateurs voient la même carte.

session_discovery_seen
├── session_id
├── inspiration_id
└── seen_at

session_discovery_current
├── session_id
├── inspiration_id
└── updated_at

Les inspirations déjà tirées dans la session sont évitées. La découverte ne crée aucune règle. ❤️ Ça me parle enregistre uniquement un intérêt Déclic pour le profil qui clique ; l'intérêt reste privé tant qu'il n'existe pas de correspondance des deux.

V1.4/V1.4.1 — Notre couple repensé

La page Notre couple n'affiche plus toutes les règles sous forme d'une longue liste de catégories ouvertes. Elle utilise :

  • un résumé du nombre d'accords et de catégories ;
  • une constellation décorative/navigationnelle sur desktop pour les principales catégories ;
  • une mosaïque de catégories qui reste lisible lorsque le socle grandit ;
  • une recherche locale dans les titres et descriptions ;
  • une vue compacte d'une catégorie avant d'ouvrir le détail d'un accord.

La V1.4.1 corrige le hotfix de constellation observé sur desktop : les huit positions commencent explicitement à nth-of-type(1) et les libellés ont une largeur maximale afin qu'aucune première bulle ne reste partiellement hors champ.

La constellation ne constitue ni un score, ni une mesure de santé du couple. Elle ne publie aucune donnée supplémentaire hors de l'état authentifié déjà autorisé.

Notifications mail

Cette section décrit le notifier historique. Dans l'état retiré, moncouple-notifier est à 0 replica et moncouple-reminder est suspendu : aucun email ou rappel Mon Couple ne doit être émis.

Avant le retrait, les notifications étaient entièrement séparées du temps réel et des fonctions d'exploration.

Depuis la V1.2, déposer une proposition ne déclenche plus de mail immédiat. Le notifier attend au moins une heure, puis envoie au maximum un rappel dédupliqué uniquement si le partenaire n'est pas en ligne et n'a encore enregistré aucun vote. Une présence récente (moins de 45 secondes) reporte le contrôle sans consommer le quota. Les attentions live, choix Déclic et découvertes surprises ne génèrent jamais d'email.

Mon Couple utilisait :

http://mail-gateway.mail-system.svc.cluster.local:8080

Le notifier détectait notamment les nouvelles propositions et le CronJob de rappel fonctionnait à 17h30 Europe/Paris en session classique. Le contenu des souhaits n'était pas inclus dans les emails.

La V1.4.2 ne modifie ni les destinataires ni la configuration SMTP, ni la logique métier du notifier ; seules ses métadonnées de version sont alignées.

Supervision

La supervision reflète désormais le retrait volontaire :

  • le healthcheck interne Mon Couple est supprimé de Gatus puisque le runtime est attendu à zéro ;
  • moncouple.djayzone.com est surveillé comme redirection HTTP 301 ;
  • moncouple.jemimah.fr est surveillé comme redirection HTTP 301 ;
  • le certificat TLS des deux anciens domaines reste surveillé ;
  • Notre Couple conserve ses propres contrôles interne et public.

Le dashboard Grafana Mon Couple reste provisionné comme vue historique/opérateur. Ses deux premiers panneaux vérifient que le nombre de replicas souhaité du web et du notifier reste à 0 ; le PVC et l'historique de consommation restent observables sans exposer de donnée relationnelle privée.

Exploitation

État attendu après retrait :

kubectl -n moncouple get deploy,pod,svc,ingress,pvc,cronjob,pdb,middleware.traefik.io
kubectl -n moncouple get deploy moncouple moncouple-notifier
kubectl -n moncouple get cronjob moncouple-reminder

Attendus : moncouple et moncouple-notifier à 0 replica souhaité, moncouple-reminder suspendu, PVC présent et Ingress/Middleware présents.

Les logs et validations applicatives ci-dessous ne sont utiles que lors d'un rollback ou d'une réactivation :

kubectl -n moncouple logs deploy/moncouple -c moncouple --tail=200
kubectl -n moncouple logs deploy/moncouple-notifier --tail=100

Validation du runtime historique avant réactivation :

python apps/ovh-primary/moncouple/validate_runtime.py
python apps/ovh-primary/moncouple/validate_frontend.py
python -m py_compile apps/ovh-primary/moncouple/*.py
python apps/ovh-primary/moncouple/test_live_experience.py
python apps/ovh-primary/moncouple/test_inspirations_catalog.py
python apps/ovh-primary/moncouple/test_session_consensus.py
python apps/ovh-primary/moncouple/test_notifier_delay.py
python apps/ovh-primary/moncouple/test_light_theme.py
python apps/ovh-primary/moncouple/test_amendment_consent.py
python apps/ovh-primary/moncouple/test_v14_exploration.py
python apps/ovh-primary/moncouple/test_v141_polish.py
python apps/ovh-primary/moncouple/test_v142_thematic_menu.py
python apps/ovh-primary/moncouple/test_v1421_nav.py
python apps/ovh-primary/moncouple/test_v15_play_hub.py
kubectl kustomize apps/ovh-primary >/dev/null
git diff --check origin/main...HEAD

V1.2 — gameplay synchronisé et navigation compacte

Toutes les mutations partagées du jeu incrémentent une révision monotone stockée dans SQLite. Le flux SSE transmet uniquement ce numéro de révision : aucun titre, description, vote ou brouillon privé n'est envoyé dans l'événement. Chaque navigateur recharge ensuite /api/state, qui conserve ses règles d'authentification et de confidentialité.

Le rafraîchissement est temporisé de 400 ms. Si un formulaire est actif ou modifié, l'interface ne le remplace pas : elle affiche un bandeau « de nouvelles modifications sont disponibles » et attend une actualisation volontaire.

Tours de proposition

Pour une session classique ou thématique, chaque joueur dispose d'une action par tour :

  • poser un souhait ;
  • ou sélectionner Je saute ce tour.

Un nouveau tour commence uniquement lorsque les deux joueurs ont agi. Le backend impose cette règle indépendamment de l'interface. Le bouton « Poser sur la table » est désactivé lorsqu'un item est déjà présent ou lorsque l'autre joueur n'a pas encore proposé/passé ; son infobulle explique la cause.

La table additive proposal_turn_actions conserve seulement le numéro de session, le joueur, le tour, l'action PROPOSE/SKIP et l'heure. Elle ne contient aucun texte de souhait.

Inspirations

Les 197 inspirations ne sont plus rendues sous forme de grandes cartes ouvertes :

  • catégories repliées par défaut ;
  • filtres horizontaux avec compteurs ;
  • recherche instantanée dans les titres et descriptions ;
  • une ligne compacte par inspiration ;
  • description et boutons d'import affichés seulement après dépliage.

Les filtres sont purement locaux au navigateur et ne modifient aucune donnée.

Observabilité et disponibilité

Historiquement, Grafana, le catalogue applicatif et le portail administrateur portaient la version publiée de Mon Couple. Depuis le retrait, ces surfaces indiquent explicitement l'état 0/redirection et Gatus vérifie les anciens domaines comme redirections plutôt que comme application disponible.

V1.2.1 — thème clair corrigé

light_theme_patch.py est appliqué après toutes les autres couches frontend afin que les styles historiques sombres ne reprennent pas la priorité. Il définit uniquement des sélecteurs sous html[data-theme="light"] : le thème sombre et le mode automatique restent inchangés.

La couche couvre notamment la navigation, les surfaces principales, les formulaires, les boutons Accepter / Négocier / Refuser, les statuts, les modales et les variantes mobiles. test_light_theme.py vérifie son ordre d'application, les contrastes critiques et l'absence de redéfinition du thème sombre.

V1.3 — amendements validés à deux

Tout nouvel amendement d'une négociation classique ou de révision est créé avec le statut PENDING. Seul l'autre profil peut ensuite l'accepter ou le refuser ; l'auteur reçoit une erreur 403 s'il tente de décider lui-même. Une décision déjà enregistrée ne peut pas être rejouée.

Un amendement accepté passe à ADOPTED et reste visible avec la proposition. Un amendement refusé passe à REJECTED et demeure dans l'historique de négociation sans être attaché à la proposition. Une reformulation est refusée tant qu'une décision est en attente.

La migration ajoute uniquement status, decided_by et decided_at aux tables d'amendements existantes. Les anciennes lignes sont marquées ADOPTED par défaut afin de préserver les accords historiques et le rollback applicatif.

test_amendment_consent.py couvre les deux types de négociation, l'interdiction d'auto-validation, l'acceptation, le refus, le blocage de reformulation et la compatibilité des données historiques.

V1.4 — stockage et confidentialité

La V1.4 ajoute uniquement des colonnes et tables SQLite :

wishes.source_inspiration_id

game_sessions.theme_categories

inspiration_choices
├── inspiration_id
├── user_id
├── choice
├── created_at
└── updated_at

session_discovery_seen
session_discovery_current

Les migrations sont idempotentes. inspiration_choices n'est volontairement pas reliée aux triggers de révision SSE globale : une réponse individuelle ne doit pas créer de signal observable dans le navigateur adverse. Seul un Déclic commun fait avancer explicitement la révision partagée afin que les deux interfaces puissent afficher le résultat.

test_v14_exploration.py exerce les endpoints avec deux sessions authentifiées et couvre la confidentialité du premier YES, les thèmes, le tirage partagé, le masquage personnel, l'adoption exacte et l'idempotence de la migration.

V1.4.1 — finition frontend

La V1.4.1 n'ajoute ni route, ni table, ni colonne. polish_v141_ui_patch.py corrige et complète uniquement l'interface générée V1.4 : filtrage thématique local, swipe Déclic, libellé personnel de découverte, Web Audio, son d'amendement, alignement de l'icône Déclic dans la barre desktop et coordonnées sûres de la constellation. test_v141_polish.py reconstruit le frontend complet, vérifie ces garde-fous et exécute la fonction de filtrage sous Node lorsqu'il est disponible.

Un rollback V1.4.1 → V1.4.0 est donc uniquement applicatif : aucune donnée à migrer ou restaurer.

V1.4.2 — entrée de session thématique fiabilisée

La V1.4.2 n'ajoute ni route, ni table, ni colonne. hotfix_v142_ui_patch.py ajoute l'entrée Session thématique directement au markup du split menu généré. Le code V1.4 qui tentait déjà de l'ajouter dynamiquement reste compatible grâce à son contrôle anti-doublon.

test_v142_thematic_menu.py reconstruit l'interface finale, vérifie que le marqueur V1.4.2 est présent exactement une fois dans la zone du menu et confirme que l'enhancer historique n'ajoutera pas de doublon.

V1.5 — hub Jouer quotidien

La page Jouer présente chaque jour les mêmes cinq suggestions aux deux profils, choisies parmi 160 activités équilibrées en huit chapitres. Le couple peut en sélectionner au maximum trois. Les activités couvrent plusieurs mécaniques de réponse, dont texte, échelle, choix, sélection multiple, classement et estimation du partenaire.

La première réponse reste privée : l'autre profil apprend uniquement que son partenaire a répondu. Les deux réponses sont révélées ensemble après le second envoi. Les marqueurs « appris » et Déclic restent eux aussi personnels jusqu'à une correspondance bilatérale ; aucune valeur privée n'est envoyée par SSE, email, métrique ou log.

Une activité terminée peut devenir un brouillon de souhait privé. Les colonnes additives wishes.source_play_activity_id et wishes.source_play_day_key conservent son origine sans publier automatiquement le souhait. Une contrainte unique par auteur, jour et activité rend cette création idempotente, y compris lors d'une répétition réseau. Les tables play_daily_items, play_daily_selections, play_answers, play_learning_marks et play_declic_choices sont créées de manière idempotente et peuvent rester présentes lors d'un rollback applicatif.

test_v15_play_hub.py couvre l'équilibre du catalogue, la stabilité du menu quotidien, la limite atomique de trois choix entre deux replicas, la confidentialité avant double réponse, les correspondances bilatérales, la progression multi-jours et l'idempotence du brouillon privé.

Dépannage V1.1+ (historique / réactivation)

Le flux SSE se reconnecte en boucle

  1. Vérifier que l'authentification classique fonctionne sur le domaine concerné.
  2. Vérifier /healthz et les logs du pod web.
  3. Vérifier qu'aucun proxy intermédiaire ne bufferise le flux ; la réponse inclut X-Accel-Buffering: no.
  4. Une reconnexion après rollout est normale : EventSource réessaie automatiquement et reprend au curseur connu.

La présence du partenaire n'apparaît pas

  • vérifier que le partenaire n'a pas activé « apparaître hors ligne » ;
  • attendre au maximum un cycle de heartbeat ;
  • après fermeture de son navigateur ou passage invisible, l'état peut rester actif jusqu'à 45 secondes ;
  • vérifier les erreurs POST /api/live/presence dans l'onglet réseau du navigateur.

Une attention n'est pas reçue

  • vérifier que le type fait partie de la liste blanche ;
  • vérifier une réponse 429 liée au rate limit ;
  • vérifier la connexion SSE ;
  • vérifier que le curseur local n'a pas été supprimé pendant une reconnexion ;
  • plusieurs onglets peuvent recevoir le même événement, ce qui est volontaire : aucun bit global « livré » n'existe.

La clôture reste à 1/2

  • vérifier que le second profil est bien connecté avec son propre domaine ;
  • vérifier /api/state et la connexion SSE ;
  • une nouvelle action métier après le premier accord remet volontairement le consensus à zéro ;
  • en session de révision, terminer ou retirer la règle active avant de demander la clôture.

Un Déclic individuel semble ne rien faire chez l'autre

C'est le comportement attendu. Le premier choix reste privé. Vérifier Nos déclics uniquement après deux réponses YES. Ne pas ajouter de log, métrique ou événement SSE qui révélerait le premier choix.

SQLite signale database is locked

Le backend utilise WAL, un timeout SQLite et un verrou de processus. Les événements live, choix Déclic et validations de clôture sont de petites transactions et le flux SSE ne conserve pas de transaction ouverte pendant l'attente.

En cas de verrou persistant :

kubectl -n moncouple logs deploy/moncouple -c moncouple --tail=200
kubectl -n moncouple get pods -o wide

Ne jamais supprimer le fichier WAL à chaud. Vérifier d'abord un éventuel pod bloqué ou une pression disque.

Un rollout est en cours

Cette procédure concerne uniquement une réactivation. Avec la configuration historique, le Deployment utilise maxUnavailable: 0; une connexion SSE peut être interrompue lorsque son ancien pod entre en preStop, puis se reconnecter sur un autre pod. Les états V1.4 partagés sont stockés dans SQLite et ne dépendent donc pas de la mémoire d'un pod.

CrashLoop après modification d'un patch

Consulter le conteneur précédent :

kubectl -n moncouple logs deploy/moncouple --previous --tail=200

Puis exécuter localement validate_runtime.py et validate_frontend.py. Les fichiers /tmp/app_runtime.py et /tmp/index.html sont générés et ne doivent jamais être édités directement.

Rollback GitOps

Le rollback du retrait consiste à revenir sur le merge qui a désactivé Mon Couple. Le PVC n'ayant pas été supprimé, aucune restauration de données n'est requise par ce changement.

Attendus après revert :

  1. Deployment moncouple restauré à 2 replicas ;
  2. Deployment moncouple-notifier restauré à 1 replica ;
  3. CronJob moncouple-reminder réactivé (suspend retiré/false) ;
  4. Middleware de redirection retiré et Ingress revenu vers le Service Mon Couple ;
  5. Gatus, dashboard, catalogue et portail restaurés à l'état actif ;
  6. validation /healthz, notifier et routes historiques par un opérateur disposant de l'accès cluster.

La V1.4 contient uniquement des migrations additives et idempotentes. Les colonnes source_inspiration_id, theme_categories et les tables de découverte peuvent rester présentes lors d'un rollback vers V1.3 ; le runtime V1.3 les ignore. Les données existantes de souhaits, votes, amendements et accords ne sont ni supprimées ni réécrites.

La V1.4.1 et la V1.4.2 n'ajoutent aucune migration. Les tables et la colonne additives V1.5 peuvent rester présentes lors d'un rollback vers V1.4.2.1 : l'ancien runtime les ignore et les données historiques ne sont pas supprimées.

Procédure GitOps :

  1. revert du commit/merge de la PR dans Git ;
  2. laisser Flux réconcilier ou forcer flux reconcile kustomization apps --with-source ;
  3. surveiller kubectl -n moncouple rollout status deployment/moncouple.

Les contrôles cluster sont à exécuter par un opérateur disposant du kubeconfig après merge. Ne jamais supprimer ou réinitialiser le PVC pour un rollback.

Fichiers principaux

apps/ovh-primary/moncouple/
├── app.py
├── app_runner.py
├── branding_patch.py
├── desktop_patch.py
├── experience_ui_patch.py
├── live_backend_patch.py
├── session_consensus_backend_patch.py
├── session_consensus_ui_patch.py
├── amendment_consent_backend_patch.py
├── amendment_consent_ui_patch.py
├── explore_v14_backend_patch.py
├── explore_v14_ui_patch.py
├── polish_v141_ui_patch.py
├── hotfix_v142_ui_patch.py
├── light_theme_patch.py
├── revision_backend_patch.py
├── revision_safety_patch.py
├── revision_ui_patch.py
├── revision_meta_patch.py
├── validate_runtime.py
├── validate_frontend.py
├── test_live_experience.py
├── test_inspirations_catalog.py
├── test_session_consensus.py
├── test_notifier_delay.py
├── test_light_theme.py
├── test_amendment_consent.py
├── test_v14_exploration.py
├── test_v141_polish.py
├── test_v142_thematic_menu.py
├── inspirations-extra.json
├── inspirations-v1.1.1.json
├── inspirations-liberte-intime-v1.1.1.json
├── index.html
├── ui_patch.py
├── notifier.py
├── VERSION
├── deployment.yaml
├── notifier-deployment.yaml
├── service.yaml
├── notifier-service.yaml
├── reminder-cronjob.yaml
├── pdb.yaml
├── middleware.yaml
├── ingress.yaml
├── pvc.yaml
├── namespace.yaml
└── kustomization.yaml