Commit Graph
56 Commits
Author SHA1 Message Date
Claude fb09bf0e3a Accueil : préciser « entreprises de service »
Sur la page d'accueil, les mentions d'« entreprises » deviennent
« entreprises de service » : texte alternatif du logo, sous-titre du
hero, phrase d'accroche et appel à adhésion (les deux variantes,
avec et sans compteur d'adhérents).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DkXREQkpPAKHFLgtnFFkCM
2026-09-14 11:04:53 +00:00
Claude dd16e05b47 Demandes & messages : boutons d'action selon l'état de la ligne
Une demande approuvée gardait ses boutons « Approuver & créer » et
« Rejeter », un message lu son « Marquer lu », un message archivé son
« Archiver » : rien n'indiquait que l'action avait déjà été faite.

Chaque ligne ne propose plus que les transitions qui ont du sens :
- demande nouvelle → Approuver & créer / Rejeter
- demande rejetée  → Rouvrir
- demande approuvée → aucun bouton (le compte existe)
- message nouveau  → Marquer lu / Archiver
- message lu       → Archiver
- message archivé  → Désarchiver

Le masquage du bouton d'approbation vit dans `ApproveRequestForm` et non
dans la page : après l'approbation, `router.refresh()` repasse la demande
en « approuvée », et un test côté serveur démonterait le bloc
d'identifiants à usage unique avant qu'il ait été lu.

Deux gardes côté serveur couvrent l'onglet resté ouvert sur l'ancien
état : `approveMembershipRequest` refuse une demande déjà traitée, et
`setRequestStatus` ne touche plus une demande approuvée.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrfrukBXnHtMwq5iT7DwAM
2026-09-13 07:48:59 +00:00
Claude e9b6fdfd91 Annuaire : cartes adhérents de hauteur uniforme
Une description longue étirait sa carte, donc toute la rangée de la
grille, et laissait les voisines avec un grand vide sous le texte.

Le titre (2 lignes) et la description (4 lignes) ont désormais une
hauteur figée et sont tronqués en fin de ligne ; un espaceur pousse
la ligne d'adresse en bas de carte, et cette adresse tient sur une
seule ligne avec ellipsis. Toutes les cartes font la même hauteur,
quel que soit le texte saisi par l'adhérent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MrfrukBXnHtMwq5iT7DwAM
2026-09-13 07:25:22 +00:00
Claude 503adcb20e Corrige la page blanche à la création d'un adhérent en doublon
« Application error: a server-side exception has occurred », digest 2117999835,
sur /backend/adherents : l'action levait `Error("Un compte existe déjà avec cet
e-mail.")`. Next masque le message d'une exception de server action en
production et remplace la page entière par son écran d'erreur — un e-mail déjà
pris, cas de saisie parfaitement ordinaire, mettait donc la page par terre.

Ces échecs attendus sont désormais **renvoyés** (`ActionError`) au lieu d'être
jetés, dans `addMember`, `inviteAdmin` et `approveMembershipRequest` :

- Le formulaire affiche la vraie raison, l'e-mail en cause inclus.
- `AddMemberPanel` réinjecte la saisie : React vide un formulaire non contrôlé
  après chaque action, il fallait sinon tout retaper.
- `InviteAdminForm` avait un rattrapage, mais devait deviner le motif faute de
  message ; il affiche maintenant celui de l'action. Son `catch` ne couvre plus
  que l'imprévu.
- `ApproveRequestForm` n'en avait aucun et tombait de la même façon.

Le `throw` reste réservé aux violations d'accès.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-09-07 07:59:25 +00:00
Claude 50b8732080 Réseaux : bouton de renouvellement du jeton, avec ses deux liens de contrôle
Renouveler un jeton demandait de deviner que « Reconnecter » servait à ça, et
les outils Meta / LinkedIn étaient enfouis sous « Comment obtenir ces
identifiants ? ». Chaque réseau connecté porte maintenant un bloc dédié :

- Un bouton « Renouveler le jeton <réseau> », qui relance l'échange OAuth
  complet (jeton court → longue durée → jeton de page).
- Les deux liens utiles à ce moment-là, chacun avec ce qu'il sert à faire :
  débogueur de jeton et outil de jetons d'accès pour Facebook, jetons de
  l'application et documentation de durée de vie pour LinkedIn.
- Le bloc passe en ambre quand le jeton est mort, expiré ou proche de l'être.

« Reconnecter » disparaît : il faisait exactement la même chose que le nouveau
bouton, à dix pixels de distance. Une fois connecté, il ne reste que
« Déconnecter » ; « Connecter » n'apparaît plus que pour la première liaison.
Les bannières d'alerte nomment désormais le bouton qui existe.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-09-06 15:33:44 +00:00
Claude 4b12dc5164 Espace adhérent : récapitulatif des dates, aperçu ramené à sa taille réelle
La colonne de droite portait un aperçu étiré sur toute sa largeur : le visuel
carré rendait la carte démesurée, très loin de ce que verra le visiteur.

- L'aperçu est borné à 320 px, la largeur d'une carte dans la grille du site.
- Au-dessus, un récapitulatif « Dates de votre offre » : durée de validité et
  moment de publication, les deux informations que la carte ne montre pas.
  Sans saisie il affiche les valeurs par défaut en gris (« Sans date limite »,
  « Dès la validation par l'association ») ; des bornes incohérentes s'y
  signalent en rouge, comme sous le champ.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-09-06 15:10:41 +00:00
Claude 353d38bebf Promotions : la page publique suit le gabarit des pages internes
La première version reprenait le style d'une *section* de l'accueil — un grand
cartouche blanc arrondi — qui détonnait à côté de l'annuaire et de
« L'association ». Elle suit maintenant le même gabarit que l'annuaire :

- `SiteHeader … logo` : toutes les pages internes affichent le logo, seule
  l'accueil s'en passe.
- Bloc de titre sur le fond beige (pastille « N offres en cours », `<h1>` au
  même `clamp`, chapeau, étincelles) au lieu du cartouche.
- Grille de cartes à même la page, `gap` aligné sur les autres listes.
- Bandeau d'adhésion bleu en pied de page, identique à celui de l'annuaire.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-09-06 14:50:41 +00:00
Claude a52e13e623 Promotions : page publique dédiée, l'accueil garde les six dernières
`/promotions` liste toutes les offres en cours ; l'accueil en montre six et
renvoie vers elle. Les deux listes partagent `PromoCard`, extrait de la page
d'accueil : un seul rendu de carte à maintenir.

- `getLivePromotions(limit)` accepte `null` pour « toutes les offres », la
  limite restant appliquée en SQL et non après coup.
- Page complète : `<h1>` unique, `<main>`, métadonnées et canonique via
  `pageMetadata`, JSON-LD `BreadcrumbList` + `ItemList`, entrée au sitemap,
  état vide qui renvoie vers l'annuaire.
- Les liens « Promotions » de l'en-tête, du pied de page et de l'espace
  adhérent visent désormais cette page. Idem pour le lien d'une publication
  réseau sans adhérent rattaché, qui pointait sur l'ancre `/#promotions`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-09-06 14:34:27 +00:00
Claude cf8cf5c8e2 Promotions : le logo revient à gauche du nom de l'adhérent
L'angle de la carte est rendu à la catégorie ; la pastille du commerce se
place devant son nom, en pied de carte, là où on lit déjà qui propose l'offre.
La période de validité passe sous le nom, dans le même bloc.

Appliqué à la carte d'accueil, à l'aperçu en direct de l'espace adhérent et à
la carte de modération. Retiré de la carte de la fiche adhérent : elle n'y
affiche pas de nom de commerce, et le logo figure déjà en tête de page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-09-06 14:22:03 +00:00
Claude d99ee7ec07 Promotions : logo de l'adhérent dans l'angle des cartes
Le logo du commerce identifie l'offre avant même qu'on lise le nom. Il est
posé en pastille ronde dans l'angle haut-gauche de la carte, la catégorie se
décalant juste à côté sur la même ligne.

- `src/components/MemberAvatar.tsx` : logo en `contain` sur fond blanc (les
  logos d'adhérents sont des marques sur fond clair, un `cover` les rognerait),
  repli sur les initiales colorées quand la fiche n'a pas de logo.
- Appliqué à la carte d'accueil, à la carte de la fiche adhérent, à l'aperçu
  en direct de l'espace adhérent et à la carte de modération : même composition
  partout, l'aperçu montre donc bien ce que verra le visiteur.
- `getLivePromotions` remonte `members.logo_url` ; la page espace le passe au
  formulaire.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-09-06 14:02:53 +00:00
Claude e980ab6e04 Promotions : publication programmée et affichage limité à la période
Deux comportements demandés :

1. Une promotion n'apparaît sur le site que pendant sa période de validité.
   Les lectures publiques passent par `VISIBLE_PROMO` (`src/lib/queries.ts`) :
   statut `live` **et** fenêtre `starts_on` / `ends_on`, journée calculée en
   Europe/Paris pour qu'un conteneur en UTC ne retire pas une offre deux
   heures trop tôt. Le statut n'est pas touché : hors période, le backoffice
   explique pourquoi l'offre n'est pas visible (`visibilityNote`) et elle
   revient d'elle-même si les dates changent.

2. La publication peut être programmée. `promotions.publish_at` : l'adhérent
   propose une date, le modérateur la garde, la déplace ou la vide dans le
   formulaire « Valider ». Avec une échéance future la promotion passe au
   statut `scheduled` — rien n'est visible, rien n'est diffusé — et
   `releaseDuePromotions()` fait la bascule à l'heure dite, site et réseaux
   ensemble. Un bouton « Publier maintenant » court-circuite l'attente.

Mécanique :
- `src/lib/promo-schedule.ts` (pur, testé) : conversion `datetime-local` ⇄
  heure de l'association, changements d'heure compris, et refus d'une saisie
  illisible plutôt qu'une publication immédiate involontaire.
- `src/lib/promo-publish.ts` reprend `publishPromoShares()` : deux appelants
  désormais, une action serveur et la boucle de libération, qui n'a pas de
  requête et ne peut donc pas appeler `revalidatePath()`. Idem pour
  `logActivity()` dans `src/lib/activity-log.ts`.
- La bascule `scheduled → live` est un `UPDATE … RETURNING` filtré sur le
  statut : atomique, donc pas de double publication même à plusieurs
  instances, en plus de la garde existante sur `social_posts`.
- Déclencheur : `src/instrumentation.ts`, boucle d'une minute avec rattrapage
  au démarrage (`PROMO_SCHEDULER=off` la désactive). Le travail vit dans
  `instrumentation-node.ts`, écarté du bundle edge par `next.config.mjs` :
  Next compile aussi l'instrumentation pour le middleware, où `pg` ne peut
  pas être embarqué.

Migration 0016 : `promo_status += 'scheduled'`, `promotions.publish_at`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-09-06 11:17:49 +00:00
Claude 02c5378a47 Promotions : période de validité (date de début / date de fin)
Une promotion peut désormais porter une date de début et une date de fin,
toutes deux facultatives et indépendantes. Sans date, rien n'est affiché.

- Schéma : `promotions.starts_on` / `ends_on` (`date`), migration 0015.
- `src/lib/promo-validity.ts` (pur, testé) : `formatValidity()` produit
  « Valable du 1er au 15 mars 2027 », « Valable jusqu'au … », « Valable à
  partir du … » en repliant mois et année quand ils se répètent ;
  `formatValidityShort()` pour les lignes de méta ; `isRangeInvalid()` pour
  la validation.
- Espace adhérent : deux champs date, aperçu en direct de la phrase et
  blocage de l'envoi si la fin précède le début.
- `publishPromo` relit et revalide les bornes côté serveur.
- Affichage : carte d'accueil, fiche adhérent, liste de l'adhérent, carte de
  modération — et le message Facebook / LinkedIn, via le même formateur.
- L'ancien texte libre `valid_until` reste comme repli d'affichage pour les
  promotions de démonstration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-09-06 11:00:44 +00:00
Claude 81b99ed5fb Réseaux : contrôle réel de validité des jetons et outils d'inspection
Un jeton de page Facebook n'a pas de date d'expiration, donc `expiryStatus`
renvoyait « never » et aucune alerte ne pouvait se déclencher. Mais il meurt
quand même : mot de passe du compte changé, rôle d'administrateur perdu sur la
page, autorisation retirée, révocation par Meta. On ne l'apprenait qu'au moment
où une publication échouait.

- `checkTokenHealth()` interroge la plateforme (`/me` côté Graph, les ACL
  d'organisation côté LinkedIn) et rend un verdict motivé.
- L'écran Réseaux sociaux contrôle à chaque affichage : pastille « Jeton
  expiré », bandeau rouge et réponse brute de la plateforme quand il est mort,
  ligne verte de confirmation sinon.
- Le verdict est persisté (`social_accounts.last_check_*`) : le tableau de bord
  alerte via `tokenHealthCached()` (fenêtre de 6 h) sans appeler la plateforme à
  chaque affichage, et croise ce signal avec la dernière tentative de
  publication refusée pour motif d'autorisation.
- Bloc « Outils pour inspecter un jeton » dans l'aide de chaque réseau :
  débogueur de jeton, outil de jetons d'accès, explorateur d'API Graph,
  documentation des jetons de longue durée — en précisant que le bouton
  Connecter fait déjà l'échange court → longue durée → jeton de page, et qu'il
  n'y a aucun jeton à copier à la main.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-09-06 10:43:11 +00:00
Claude 17511a73c3 Connexion réseaux : adresse de retour déduite de la requête, CSP sans upgrade en HTTP
Cliquer sur « Connecter » sans avoir enregistré l'URL publique du site
renvoyait une erreur, et la redirection retombait sur https://0.0.0.0 :
la directive upgrade-insecure-requests était posée même sur une page
servie en HTTP, le navigateur basculait donc la navigation vers un
https:// inexistant.

- `publicBaseUrl()` : réglage `site_public_url`, variables d'environnement,
  puis origine de la requête (X-Forwarded-Host / X-Forwarded-Proto). L'adresse
  de retour OAuth et le lien des publications l'utilisent ; `seo-server`
  s'appuie dessus au lieu de dupliquer la déduction.
- `src/lib/site-url.ts` : déduction pure et testée de l'origine, HTTP par
  défaut pour localhost et les IP nues, HTTPS pour un nom de domaine.
- Middleware : `upgrade-insecure-requests` uniquement quand la page est servie
  en HTTPS.
- Page Réseaux : l'adresse détectée est annoncée comme valeur par défaut, avec
  le rappel que Facebook et LinkedIn exigent du HTTPS sur un domaine.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-09-06 10:11:06 +00:00
Claude 23d1d14a49 Espace adhérent en deux pages, e-mail de contact, logo sans débordement, types de produits pour les promos
- Espace adhérent : /backend/espace (profil, inscriptions, droit à l'image)
  et /backend/espace/promotions (dépôt et suivi). Bandeau et onglets communs
  (EspaceHeader) avec le nombre d'offres en ligne ; la page promotions ouvre
  sur « Mes promotions en cours », puis le formulaire, puis les offres en
  attente et l'historique. Deux entrées dans le menu, titres de page dédiés,
  revalidation des deux chemins.
- Fiche adhérent : colonne members.contact_email (migration 0013), saisie
  dans l'espace adhérent et la fiche staff ; la fiche publique et le JSON-LD
  affichent contact_email, sinon l'e-mail du compte.
- VitrineImage : sans photo de couverture, le logo est contenu à ~60 % d'un
  cadre à hauteur fixe sur un fond du logo flouté ; le conteneur sans hauteur
  qui laissait le logo déborder est supprimé.
- Promotions : la catégorie devient un type de produit ou de service
  (src/lib/promo-categories.ts, 10 groupes, ~85 entrées) pré-sélectionné
  depuis le métier de l'adhérent, à la place de la liste des métiers.
- Tests : catégories de promo (largeur, unicité, pré-sélection pour chaque
  métier du référentiel).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDbKbJrPWweXXYSxA8MXW7
2026-09-06 09:02:27 +00:00
Claude 8b46cd6316 Merge remote-tracking branch 'origin/main' into claude/seo-improvement-eilfsl 2026-09-06 06:56:00 +00:00
Claude bfc82b3b3b Tags adhérents automatiques : vocabulaire par métier, suggestions et repli
- src/lib/tags.ts (pur) : mots-clés par métier pour les 104 catégories,
  vocabulaire transversal détecté dans la description (fait maison, bio,
  livraison, sur rendez-vous, devis gratuit…), suggestTags() qui combine
  métier, commune et description, autoTags() qui n'intervient qu'à vide.
- Enregistrement : addMember, updateMember et updateOwnProfile passent par
  resolveMemberTags() ; un champ laissé vide reçoit les suggestions, une
  saisie n'est jamais remplacée.
- Formulaires : composant TagsField (champ non contrôlé + pastilles de
  suggestions recalculées à chaque frappe depuis catégorie, commune et
  description ; « Tout ajouter »), dans l'espace adhérent et la fiche staff.
- Fiche publique : les fiches jamais ré-enregistrées affichent les tags
  suggérés et les transmettent dans LocalBusiness.keywords.
- tests/tags.test.ts : couverture de chaque métier, nettoyage, détection,
  ordre des suggestions, non-écrasement.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDbKbJrPWweXXYSxA8MXW7
2026-09-06 06:51:25 +00:00
Claude c8417d8560 SEO : URLs de fiche parlantes, pages métier indexables, aides de rédaction
- Fiches adhérents : URL canonique /adherents/12-au-bon-pain-frouard
  (memberPath). L'identifiant en tête suffit ; /adherents/12 et tout slug
  périmé sont redirigés de façon permanente vers la forme canonique, donc
  les anciens liens et les partages sociaux restent valides. Tous les liens
  (accueil, carrousel, annuaire, JSON-LD, sitemap, publications Facebook /
  LinkedIn) passent par memberPath.
- Pages métier /annuaire/[categorie] : une page indexable par catégorie
  avec titre, introduction (effectif, communes), grille d'adhérents, fil
  d'Ariane et ItemList. Sans adhérent : noindex et hors sitemap. Section
  « Parcourir par métier » sur l'annuaire et « Autres métiers » sur chaque
  page pour le maillage interne.
- Composants partagés MemberCard et CategoryLinks ; slugify déplacé dans
  src/lib/slug.ts.
- Backend : conseils de rédaction sous les champs description et tags des
  formulaires adhérent (espace adhérent et fiche staff).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDbKbJrPWweXXYSxA8MXW7
2026-09-06 06:29:28 +00:00
Claude f9d0d6c825 Accueil, pied de page et catégories : compteur réel, boutons alignés, référentiel complété
- Accueil : le bandeau « Faites partie de l'aventure » affiche le nombre
  d'adhérents validés (statut active) au lieu de « plus de 120 » codé en dur.
- Pied de page : les boutons Facebook / LinkedIn ont la même largeur (grille à
  une colonne dimensionnée sur le plus long) et une icône à largeur fixe.
- Catégories : référentiel unique dans src/db/categories.ts (104 métiers, dont
  assurance, bazar/discount, grande distribution, librairie-presse, tabac,
  vétérinaire, auto-école, etc.), inséré par la migration 0012 avec
  ON CONFLICT (slug) : les catégories manquantes apparaissent au prochain
  démarrage, sans écraser un libellé renommé depuis le backend. Le seed et
  l'action « Ajouter une catégorie » réutilisent ce référentiel et sa palette.
- tests/categories.test.ts vérifie unicité des slugs, contraintes de la table
  et cohérence entre le référentiel et la migration.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDbKbJrPWweXXYSxA8MXW7
2026-09-06 06:18:37 +00:00
Claude 320e63b749 Caddy : répondre aussi sur l'IP, pour installer avant que le DNS ne soit posé
Le Caddyfile gagne un bloc fourre-tout http:// à côté du bloc du nom de
domaine. L'installation complète, Caddy compris, devient donc utilisable
immédiatement sur http://IP_DU_SERVEUR, et bascule seule en HTTPS dès que le
DNS pointe sur la machine : Caddy réessaie la demande de certificat en
arrière-plan, sans intervention ni redémarrage.

Le préfixe http:// désactive le HTTPS automatique pour ce seul bloc, aucune
autorité ne délivrant de certificat pour une IP nue. Le bloc du domaine reste
prioritaire et sa redirection HTTP vers HTTPS est insérée avant le fourre-tout.
Vérifié avec le binaire Caddy 2.8.4 : configuration valide, et à l'exécution un
308 vers https:// pour l'en-tête Host du domaine contre un passage au proxy
pour l'accès par IP.

Rend inutile la surcouche docker-compose.http.yml, retirée, la surcouche Caddy
couvrant désormais les deux situations. README et .env.example décrivent ce
parcours unique.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8UUiQtBWYWNFcsea8spnX
2026-09-05 15:26:40 +00:00
Claude 8414c7201e Installation VPS : surcouche HTTP port 80 pour valider avant le domaine
Ajoute docker-compose.http.yml, qui publie l'application sur le port 80 et
laisse Compose créer le réseau proxy. Permet de valider une installation sur
l'IP du serveur avant que le DNS ne soit en place, sans HTTPS possible à ce
stade (pas de certificat pour une IP nue).

Documente les deux phases dans le README et .env.example : HTTP simple d'abord,
puis bascule vers la surcouche Caddy en changeant la seule ligne COMPOSE_FILE,
les volumes conservant base, compte admin et secret de session.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8UUiQtBWYWNFcsea8spnX
2026-09-05 15:21:31 +00:00
Claude 0562d07254 Installation VPS sans reverse proxy : Caddy + Let's Encrypt en surcouche Compose
Ajoute docker-compose.caddy.yml et un Caddyfile pour servir le site sur un
nom de domaine en 80/443 avec certificat automatique, sans Nginx Proxy
Manager. Le port 8413 n'est plus publié dans ce mode et le réseau proxy est
créé par Compose. Procédure documentée dans le README et .env.example.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8UUiQtBWYWNFcsea8spnX
2026-09-05 15:08:45 +00:00
Claude a8adb9ae6a Référencement : métadonnées complètes, données structurées, sitemap et robots
- Layout : metadataBase, gabarit de titre « %s · Plein R », description,
  mots-clés, Open Graph / Twitter, robots, icônes, manifeste, viewport et
  couleur de thème ; JSON-LD Organization + WebSite (recherche annuaire).
- Chaque page publique pose titre, description (≤ 160 caractères) et
  canonique via pageMetadata() ; l'annuaire ignore ainsi ?q=. Fiche adhérent :
  titre « Nom · Catégorie à Ville », description dérivée, LocalBusiness avec
  adresse, contacts et horaires (OpeningHoursSpecification), BreadcrumbList.
  Annuaire : ItemList des adhérents. Association : BusinessEvent par
  rencontre à venir. Rencontres passées : fil d'Ariane.
- Nouvelles routes : robots.txt, sitemap.xml (pages statiques + adhérents
  actifs), manifest.webmanifest, image de partage 1200×630 générée avec le
  logo, page 404 dédiée (noindex).
- /backend, /login, /inscription/* et les fiches inactives en noindex.
- Landmarks <main> sur toutes les pages publiques, fil d'Ariane en <nav>.
- serializeJsonLd() échappe <, > et & : les saisies des adhérents ne peuvent
  pas fermer le bloc <script>. Tests unitaires dans tests/seo.test.ts.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HDbKbJrPWweXXYSxA8MXW7
2026-09-05 10:41:53 +00:00
Claude 741dd53b40 Base vide par défaut : données de démonstration isolées et purgeables
Le seed ne crée plus que le référentiel des catégories et le compte
administrateur initial. Les adhérents, promotions, demandes d'adhésion,
entrées du journal et comptes de démonstration sont déplacés dans
src/db/demo-data.ts et ne sont insérés qu'avec SEED_DEMO=true.

Nouveau script `npm run db:purge-demo` (bundlé en dist/purge-demo.cjs pour le
conteneur) : retire exactement ce jeu de démonstration d'une base qui l'a reçu
lors des démarrages précédents, sans toucher aux saisies de l'association ni
aux catégories. Idempotent, en transaction ; un compte réel rattaché à une
fiche de démo est détaché et non supprimé.

Vérifié en local sur Postgres 16 : seed par défaut = 59 catégories + 1 admin
et rien d'autre ; seed avec SEED_DEMO=true = jeu complet ; purge = retour à
l'état vide ; seconde purge sans effet.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RAQsCp4nnZbwexCg7NDBHE
2026-09-04 09:23:44 +00:00
Claude 3b17d7ff42 Durcissement (lot A) : seed sûr en production, mots de passe temporaires affichés une seule fois
Audit de sécurité complet de l'application, puis correctifs du lot A validés :

- Seed : plus aucun compte de démonstration (mot de passe « changeme123 »)
  sans SEED_DEMO=true. L'administrateur initial reçoit un mot de passe
  aléatoire affiché une fois dans les journaux (ou SEED_ADMIN_PASSWORD) et
  doit le changer à la première connexion. docker-compose ne fournit plus de
  mot de passe par défaut.
- Mots de passe temporaires : la colonne users.temp_password (en clair) est
  supprimée (migration 0011). Création d'adhérent, réinitialisation,
  rattrapage des comptes manquants, approbation de demande et invitation
  staff renvoient les identifiants, affichés une seule fois par le composant
  OneTimeCredentials, sans redirection. L'invitation staff, qui ne
  communiquait jamais le mot de passe, redevient utilisable et impose le
  changement à la première connexion.
- Sessions JWT limitées à 7 jours (30 auparavant).
- En-têtes : Strict-Transport-Security ajouté, X-Powered-By supprimé.
- docker-compose : port Postgres publié sur 127.0.0.1 uniquement.
- Image Docker sur node:22 (Node 20 en fin de vie).
- Dépendances : next 15.5.25, pg 8.23, nanoid 3.3.18 (avis GHSA-2v37-7h3g-55p8).

Vérifié en local : tests, typage, build de production, migration + seed sur
un Postgres 16, et parcours navigateur complet (première connexion, changement
forcé, création / réinitialisation / invitation avec affichage unique, cookie
de session à 7 jours).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RAQsCp4nnZbwexCg7NDBHE
2026-09-04 09:15:45 +00:00
Claude dd7bbfb777 Refonte du pied de page
Trois colonnes dont une très large laissaient le tiers droit vide, les titres ne
se distinguaient pas des liens, et les deux icônes sociales flottaient en bas à
gauche sans rattachement.

- Quatre colonnes : marque, Accès rapide, Informations, Nous suivre. La largeur
  est enfin occupée, et la dernière colonne accueille aussi les coordonnées de
  l'association dès qu'elles sont renseignées dans Paramètres.
- Titres en capitales espacées sur le brun de la marque : ils reprennent le
  vocabulaire des pastilles utilisées partout ailleurs sur le site, et se lisent
  désormais comme des titres.
- Icônes sociales remplacées par des boutons libellés, calés à gauche pour ne
  pas s'étirer en pavés vides.
- Filet doré en haut du bloc, pastille dorée devant « Bassin de Pompey » : la
  page se referme au lieu de s'arrêter sur une ligne grise.
- Liens plus aérés, décalage léger au survol.
- Deux entrées utiles ajoutées : Rencontres passées et Espace adhérent.
- Replis : 3 colonnes sur tablette, 2 sur téléphone avec la marque et les
  réseaux sur toute la largeur.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-07-25 13:11:29 +00:00
Claude b0788d3f20 Durcissement : mot de passe actuel, sessions révocables, dépendances, CSP, tests
Suite de l'audit : les cinq points laissés en suspens sont traités.

Mot de passe
- `changeOwnPassword` exige désormais le mot de passe actuel. Un poste laissé
  ouvert ne suffit plus à s'approprier un compte. Les erreurs reviennent sur
  l'écran avec un message au lieu d'une page d'erreur brute.

Sessions révocables
- Colonne `users.session_version`, portée dans le jeton et comparée à la base.
- `getSession()` (src/lib/session.ts) remplace `auth()` sur les 20 pages et
  actions : rôle, rattachement adhérent et existence du compte sont relus à
  chaque requête. Supprimer un compte ou réinitialiser un mot de passe coupe
  immédiatement les sessions ouvertes, sans attendre l'expiration du jeton.

Dépendances — de 8 vulnérabilités (2 critiques) à zéro
- next 15.5.19 → 15.5.21, next-auth beta.25 → beta.32, drizzle-orm 0.38 → 0.45.
- postcss et sharp forcés par `overrides` sur leurs versions corrigées, Next ne
  les ayant pas encore reprises ; drizzle-kit et esbuild montés côté outillage.
- `npm audit fix --force` a été écarté : il proposait de RÉTROGRADER Next en
  9.3.3 et eslint-config-next en 12, ce qui aurait cassé l'application.
- `eslint-config-next` traînait dans node_modules sans être déclaré : retiré.

CSP complète
- Politique à nonce posée par le middleware, nonce régénéré à chaque requête,
  `script-src` sans 'unsafe-inline'. `style-src` garde 'unsafe-inline' : tout le
  design repose sur des attributs style, et une injection de style n'a pas la
  portée d'une injection de script.
- Conséquence assumée : rendu dynamique pour toutes les pages, un HTML
  pré-généré ne pouvant pas porter de nonce.

Tests
- `npm test` (runner natif node:test via tsx), 18 tests sur le filtre XSS, la
  limitation des tentatives de connexion et le chiffrement des jetons.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-07-25 10:18:04 +00:00
Claude 2874d79c9e Correctifs de sécurité : XSS stocké, SSRF, force brute, aléa des mots de passe
Audit de l'application. Cinq corrections, la première critique.

XSS stocké non authentifié (critique)
- Le journal d'activité était rendu avec `dangerouslySetInnerHTML` alors qu'il
  agrège des saisies de tiers : titre de promotion d'un adhérent, et surtout nom
  laissé dans le formulaire de contact PUBLIC. N'importe quel visiteur pouvait
  donc faire exécuter du script dans le navigateur d'un administrateur, et
  prendre la main sur son compte.
- Défense aux deux bouts : `sanitizeActivityMessage` ne laisse passer que
  <strong> à l'écriture, et `activityNodes` rend le message en éléments React —
  un message déjà stocké en base ne peut plus rien injecter.

SSRF
- `loadPromoImage` allait chercher toute URL http(s) placée dans le champ image
  d'une promotion, et en poussait le contenu vers Facebook ou LinkedIn : de quoi
  atteindre un service interne ou les métadonnées de l'hébergeur. Seules les
  data-URI sont désormais acceptées, ce qui couvre tous les usages réels.
- Les images sont en plus validées à l'écriture (type et taille), côté promotion
  comme côté profil adhérent.

Authentification
- Aucun frein sur la connexion : blocage progressif après 8 échecs par compte.
- La comparaison bcrypt est menée même sans compte correspondant, sinon le temps
  de réponse révélait quelles adresses existent.

Aléa
- Les mots de passe temporaires venaient de `Math.random()`, prédictible :
  passage à `randomInt` de node:crypto, et longueur portée à 12.

En-têtes
- nosniff, X-Frame-Options, Referrer-Policy, Permissions-Policy et
  frame-ancestors. Une CSP complète demandera des nonces sur les scripts Next.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-07-25 09:02:25 +00:00
Claude fb3d5e337e Page Association : mission développée et bloc « ce que l'association apporte »
Le bandeau Mission tenait en une phrase et ne disait pas ce que l'adhésion
apporte concrètement.

- Mission réécrite en trois paragraphes : le réseau et les rencontres, la mise
  en avant de chaque adhérent auprès des habitants et des professionnels, et
  l'objectif double clients de proximité / courants d'affaires locaux.
- Nouveau bloc de quatre cartes sous le bandeau : se rencontrer, gagner en
  visibilité (site et réseaux sociaux), attirer des clients en B to C,
  développer des affaires en B to B.
- Les deux contenus sont éditables dans Paramètres : la mission accepte des
  paragraphes séparés par une ligne vide, les apports suivent la convention
  « Titre|Description » déjà utilisée pour le directoire.
- Migration 0009 : le texte de mission n'est remplacé que s'il est resté celui
  d'origine, une version réécrite à la main n'est jamais écrasée.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-07-25 08:36:51 +00:00
Claude e86c67c17e URL publique du site réglable depuis le backoffice
Connecter un réseau social imposait de poser NEXT_PUBLIC_SITE_URL dans
docker-compose et de redémarrer le conteneur. L'écran Réseaux sociaux porte
désormais le champ, pré-rempli avec l'adresse par laquelle l'administrateur
consulte la page.

- Nouveau réglage `site_public_url` ; `siteUrl()` lit la base puis retombe sur
  les variables d'environnement, donc les déploiements actuels ne bougent pas.
- `siteUrl()`, `redirectUri()`, `authorizeUrl()`, `promoLink()` et
  `buildPromoMessage()` deviennent asynchrones.
- Seule l'origine est conservée : le schéma est ajouté s'il manque, un chemin
  est retiré, et une adresse invalide revient sur l'écran avec un message au
  lieu d'une page d'erreur brute.
- `saveSiteSettings` saute cette clé : le formulaire Paramètres ne la contient
  pas et l'aurait écrasée à chaque enregistrement.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-07-25 08:28:04 +00:00
Claude db361051e6 Menu d'administration regroupé et version mobile du site
Le menu empilait douze liens sous un seul intitulé, avec des carrés, ronds et
losanges qui ne portaient aucun sens. Sur téléphone, la barre latérale se
dépliait en ligne et devenait un pavé de boutons inutilisable.

Navigation
- Six rubriques au lieu d'une pile : Pilotage, Vie du réseau, Rencontres,
  Communication, Configuration, Côté adhérent. Le quotidien est ainsi séparé de
  ce qu'on ne touche qu'occasionnellement.
- Une icône par entrée (`BackendIcons.tsx`) à la place des formes génériques.
- Le menu devient une table de données filtrée par capacité, au lieu de douze
  blocs JSX répétant les mêmes appels à `can()`.

Mobile
- Sous 1024px la barre latérale devient un tiroir : voile de fond, fermeture par
  Échap, par clic hors zone et à la navigation, défilement de la page bloqué,
  actions Voir le site / Déconnexion rapatriées dans le tiroir.
- `min-height: 0` sur la zone de navigation : sans lui l'enfant flex refusait de
  rétrécir et la carte utilisateur recouvrait les derniers liens, interceptant
  les clics.
- En-tête public repensé : logo toujours présent sur téléphone (seul retour à
  l'accueil), accès adhérent sur la même ligne, navigation répartie en dessous
  au lieu de se replier en escalier.
- Liste des adhérents : le tableau à cinq colonnes devient une fiche par
  adhérent, sur deux lignes constantes, sans défilement horizontal.
- Compteurs du tableau de bord en deux colonnes plutôt qu'empilés.

Vérifié : aucun débordement horizontal sur les 23 pages en 320, 390 et 768 px ;
tiroir et cibles tactiles testés au doigt sur toutes les interactions.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-07-25 00:25:56 +00:00
Claude 19d1104ab0 Connexion Facebook / LinkedIn en OAuth depuis le backoffice
Les jetons se posaient à la main dans docker-compose. Un écran Backend ›
Réseaux sociaux permet désormais de coller les identifiants de l'application,
cliquer Connecter, choisir la page, et ne plus y revenir.

- Table `social_accounts` : identifiants, jetons et cible par réseau. Tous les
  secrets sont chiffrés en AES-256-GCM (`src/lib/crypto.ts`), clé
  `SOCIAL_TOKEN_KEY` avec repli sur `AUTH_SECRET`.
- Routes `api/social/[network]/{connect,callback}` : `state` anti-CSRF en cookie
  httpOnly, échange du code, récupération des pages administrées. Aucun jeton ne
  transite par une URL — les cibles sont relistées côté serveur au moment de la
  sélection.
- Facebook : jeton utilisateur longue durée puis jeton de PAGE, qui n'expire
  pas. LinkedIn : jeton 60 jours, rafraîchi automatiquement si l'application a
  obtenu les jetons de rafraîchissement programmatiques.
- Faute de quoi le backoffice affiche la date d'expiration, un bandeau sur le
  tableau de bord à J-7 et un bouton Reconnecter.
- `social.ts` lit les identifiants via `social-accounts.ts` : base d'abord,
  variables d'environnement ensuite. Les installations existantes continuent de
  fonctionner sans modification.
- Le secret d'application n'est jamais renvoyé au navigateur : champ vide =
  valeur conservée.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-07-24 23:00:08 +00:00
Claude 8bc1b17fc5 Diffusion réseaux pilotée par la validation de la promotion
Le bouton libre « Publier sur X » permettait de diffuser n'importe quelle promo
en ligne, à tout moment. Le choix appartient désormais à l'adhérent, la
publication part à la validation, et le choix est figé ensuite.

- Colonnes `promotions.share_facebook` / `share_linkedin` : réseaux demandés à la
  soumission, modifiables tant que la promo est `pending`.
- L'adhérent coche les réseaux dans le formulaire de soumission et peut encore
  les changer depuis Mes promotions tant qu'elle est en attente ; une fois
  validée, il ne voit plus qu'un état en lecture seule.
- Le modérateur retrouve ces cases pré-cochées dans le formulaire « Valider » et
  peut les ajuster : c'est le dernier moment où la diffusion est modifiable.
  Valider met en ligne puis publie, dans cet ordre, afin qu'un réseau en panne
  ne bloque pas la mise en ligne.
- `publishPromoShares` devient le seul point de publication. Elle ne lève jamais
  et ignore tout réseau ayant déjà une publication réussie : une promo n'est
  jamais postée deux fois, y compris après un cycle suspension → remise en ligne.
- `sharePromoToSocial` est remplacée par `retryPromoShare`, limitée au rattrapage
  d'un échec sur un réseau déjà choisi, sur une promo en ligne uniquement.
- Un réseau non configuré sur le serveur enregistre un échec explicite plutôt
  que d'échouer en silence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-07-24 22:41:25 +00:00
Claude e3a4771edf Refonte du bloc infos pratiques, suspension des promos et publication réseaux
Bloc « Informations pratiques » (fiche adhérent)
- Suppression des dégradés hors palette (radial doré de la section, dégradés
  crème/bleu des deux sous-cartes) au profit du vocabulaire visuel du site :
  carte blanche bordée, sous-cartes plates sur le fond de champ, variables CSS.
- La pastille « Préparer votre visite » devient une puce arrondie identique aux
  badges du reste du site ; le chip d'ouverture reprend les teintes de statut.
- Les glyphes texte ◷ et ↗ sont remplacés par des icônes SVG cohérentes.

Suspension des promotions
- Nouveau statut `suspended` + colonnes `suspended_by` / `suspended_by_id` /
  `suspended_at`.
- L'adhérent suspend et réactive ses propres promotions depuis Mon espace ;
  l'association suspend et réactive n'importe laquelle depuis le backoffice.
- Une suspension décidée par l'association ne peut pas être levée par
  l'adhérent : le motif est affiché dans son espace.
- Les lectures publiques filtrant déjà sur `live`, une promo suspendue quitte
  immédiatement l'accueil, l'annuaire et la fiche.

Publication Facebook / LinkedIn
- `src/lib/social.ts` : publication d'une promo en ligne sur la page Facebook
  (Graph API, upload multipart car les images sont en data-URI) et sur la page
  LinkedIn (Posts API + Images API en trois étapes).
- Jetons d'accès exclusivement en variables d'environnement ; un réseau non
  configuré masque son bouton.
- Table `social_posts` : chaque tentative est tracée, un échec est rattrapé et
  affiché sur la carte de la promo sans interrompre le backoffice.
- Nouvelle capacité RBAC `publishSocial` (admin + modérateur).

Liens sociaux publics
- Réglages `association_facebook` / `association_linkedin` éditables dans
  Paramètres, préremplis avec les pages de l'association.
- Section « Plein R sur les réseaux » sur l'accueil et icônes dans le pied de
  page, masquées si le réglage est vide.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXNXRC4j5VLfKvpyyisrnb
2026-07-24 22:20:51 +00:00
Claude b0154d6fc1 Vitrine sans photo : logo encadré dans le thème (fini le fond blanc nu)
Quand un adhérent n'a pas de photo de couverture/vitrine, le logo était affiché
sur fond blanc brut. Il est désormais présenté dans une carte blanche encadrée
(bordure crème + ombre douce) sur fond crème dégradé — cohérent avec le thème.
S'applique à la fiche, l'annuaire et le carrousel.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-28 06:47:08 +00:00
Claude a4915b445d Cartes promo : cadre image carré 1:1 (adapté aux visuels Facebook)
Les visuels promo sont carrés (posts Facebook) : un cadre 1200/630 laissait des
bandes floues sur les côtés. Passage du cadre image en 1:1 partout (accueil,
fiche, aperçu espace adhérent, modération) → les images carrées remplissent
sans bords. Conseil d'upload : image carrée (1080×1080).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-27 21:18:18 +00:00
Claude 7037e8863b Cartes promo : zone image en aspect-ratio 1200/630 (nouvelle maquette)
Aligne les cartes promo sur la dernière refonte Claude Design : la zone image
passe d'une hauteur fixe à un ratio 1200/630, au-dessus du composant PromoImage
existant (remplissage flou + contain). Appliqué à l'accueil, la fiche adhérent,
l'aperçu de l'espace adhérent et la modération. Conseil d'upload : 1200×630.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-27 15:54:22 +00:00
Claude e2cf03c9f3 Anime la page d'accueil : apparitions au défilement + étoiles en mouvement
- Composant Reveal (IntersectionObserver) + keyframes CSS : sections et grilles
  apparaissent en fondu/glissement, cartes en cascade (stagger). Respecte
  prefers-reduced-motion + repli no-JS.
- Étoiles (sparkles) : l'animation twinkle inclut désormais une dérive
  (translation) pour un mouvement nettement visible.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-26 22:09:43 +00:00
Claude 9c4bdbe09d Élargit la barre de recherche de l'accueil jusqu'au bord droit
Le formulaire passe en pleine largeur (display:flex, width:100%) et le champ
occupe l'espace disponible (flex:1).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-26 22:02:26 +00:00
Claude 856fbc1c90 Gestion des catégories dans le back-office (admin)
Nouvelle page /backend/categories : lister, ajouter (slug + couleur auto),
renommer et supprimer des catégories. Capacité RBAC manageCategories (admin) ;
lien ajouté à la barre latérale. La suppression détache les adhérents concernés
(transaction) pour éviter toute violation de clé étrangère.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-26 21:58:46 +00:00
Claude 4ab05c5d71 Élargit fortement la liste des catégories d'adhérents (59) + select dynamique
Seed enrichi avec ~59 catégories (bouche, mode/beauté, maison/bâtiment, auto,
santé, services aux particuliers & entreprises, loisirs/culture, industrie),
slugs existants conservés. Le select « Catégorie » de l'espace adhérent lit
désormais la liste depuis la base (plus de liste codée en dur). Insertion
idempotente : relancer le seed ajoute les nouvelles catégories sans doublon.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-26 21:52:49 +00:00
Claude e5ce8ab528 Retire le logo + titre de l'en-tête (haut-gauche) et du pied de page (bas-gauche)
Le menu reste aligné à droite et le bloc contact à droite, conformément aux
maquettes d'origine.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-26 21:43:45 +00:00
Claude 5b6445b571 En-tête/pied de page partagés et cohérents + retrait du bloc métiers
- Menu identique sur toutes les pages publiques (Accueil, Annuaire, Promotions,
  L'association, Espace adhérent) via SiteHeader/SiteFooter : on peut toujours
  revenir à l'accueil depuis l'annuaire et inversement (page courante en gras).
- Retire la section « Parcourir par métier » de la page d'accueil.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-26 21:34:30 +00:00
Claude ef6b233c95 Fiche adhérent complète, fidèle à la maquette
Enrichit la fiche : couverture + avatar, identité (catégorie, localisation,
adhérent depuis, boutons Appeler/Itinéraire), à propos multi-paragraphes + tags,
informations pratiques (horaires + contact tél/email/site), carte Google Maps,
colonne promotions en cours et CTA contact.

Ajoute les champs members: cover_url, phone, website, postal_code, member_since,
hours, tags (migration 0001) ; éditables dans le back-office ; seed enrichi
(Au Bon Pain conforme à la maquette).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-26 21:25:45 +00:00
Claude a6718c90f1 Ajoute la page Annuaire publique (/annuaire) reliée aux fiches adhérents
Liste des adhérents actifs avec recherche (nom/métier), filtre par commune,
filtres catégories (chips + compteurs), tri (A–Z / avec promotions), état vide
et CTA d'adhésion — fidèle à la maquette. Chaque carte ouvre la fiche adhérent
(/adherents/[id]). La recherche de l'accueil et la nav pointent vers /annuaire
(la barre de recherche pré-remplit via ?q=). Nouvelles requêtes annuaire.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-26 21:13:59 +00:00
Claude 3a6273c5b4 Ajoute la page fiche adhérent publique (/adherents/[id])
Profil public d'un commerçant : vitrine, catégorie, localisation, description,
bouton de contact et promotions en cours. Réservée aux adhérents actifs (404
sinon). Les cartes « À l'honneur » de l'accueil pointent désormais vers la fiche.
Nouvelles requêtes getPublicMember / getMemberLivePromotions.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-26 21:06:02 +00:00
Claude 39221fd7ef Corrige la suppression d'adhérent (FK) et intègre le vrai logo
- deleteMember: supprime d'abord les promotions liées et détache les comptes
  utilisateurs (transaction) avant de supprimer l'adhérent, ce qui évite la
  violation de clé étrangère (erreur serveur au clic « Supprimer »).
- Logo: remplace le SVG provisoire par le logo officiel (public/assets/logo.png)
  et met à jour toutes les références (accueil, login, backend, favicon).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-26 18:26:31 +00:00
Claude 51f5691f45 Corrige la redirection login derrière un reverse proxy (AUTH_URL)
La redirection vers /login utilisait http://localhost:8413 (ancienne valeur par
défaut d'AUTH_URL). On retire ce défaut localhost : AUTH_URL prend la valeur
fournie (ex. https://pleinr.ffnancy.fr) et pilote la redirection. Ajout de
AUTH_TRUST_HOST=true (confiance aux en-têtes X-Forwarded-*) et l'entrypoint
supprime AUTH_URL si vide. Vérifié : la redirection pointe vers le domaine public.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-26 17:25:31 +00:00
Claude e9f5d4ced5 Connecte Postgres via le nom de conteneur unique (pleinr-postgres)
Le nom générique "postgres" entrait en collision avec d'autres conteneurs
"postgres" présents sur le réseau proxy partagé (nginx_default), d'où les
ECONNREFUSED vers de mauvaises IP. On utilise désormais pleinr-postgres,
unique et présent uniquement sur le réseau interne.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-26 17:02:11 +00:00
Claude 41d2cd785e Réseaux explicites : app et Postgres partagent un réseau interne dédié
Corrige ECONNREFUSED vers postgres:5432 apparu après l'ajout du réseau proxy.
Postgres et l'app sont désormais tous deux sur un réseau "internal" dédié
(résolution fiable de l'hôte "postgres"), l'app étant en plus sur le réseau
externe "proxy" pour le reverse proxy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-26 16:58:41 +00:00
Claude 785f7169cc Attache l'app au réseau externe du reverse proxy (NPM)
Le conteneur app rejoint son réseau interne (accès Postgres) ET le réseau
externe partagé avec Nginx Proxy Manager, afin que NPM puisse l'atteindre par
nom (pleinr-app:3000) sans 504. Nom du réseau paramétrable via PROXY_NETWORK
(défaut: nginx_default).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-26 16:54:30 +00:00
Claude 71fb0ec032 Corrige l'échec du build Docker : initialisation paresseuse du client DB
`next build` collecte les données de page en important les modules serveur sans
DATABASE_URL ; l'ancien client jetait une erreur dès l'import. Le client Drizzle
est désormais initialisé paresseusement (via un Proxy) à la première requête,
donc l'import est sans effet de bord et le build n'exige plus DATABASE_URL.
Vérifié : build OK sans variables d'env, et requêtes runtime OK via le proxy.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-26 16:35:38 +00:00
Claude 718a704868 Rend AUTH_SECRET optionnel : génération auto + persistance au déploiement
Le compose ne requiert plus AUTH_SECRET (`:?` retiré). Si la variable est vide,
docker-entrypoint.sh génère un secret aléatoire et le persiste dans le volume
app-data (/app/data/auth_secret), réutilisé aux redémarrages. Surcharge possible
en définissant AUTH_SECRET. Déploiement Portainer sans configuration requise.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-26 16:26:26 +00:00
Claude 639c0614cd Change le mapping des ports hôte vers des ports peu courants
- App : 8413 (hôte) → 3000 (conteneur)
- Postgres : 54329 (hôte) → 5432 (conteneur)
AUTH_URL, .env.example et README mis à jour en conséquence. Les ports internes
des conteneurs (3000, 5432) et DATABASE_URL (réseau interne) restent inchangés.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-26 16:23:49 +00:00
Claude cd4dba9be4 Relie l'historique initial de main (placeholder README) à la branche Plein R 2026-06-26 15:28:18 +00:00
Claude 7e22ec50d4 Implémente le site Plein R (Accueil + Backend) sur Next.js + Postgres + Docker
Application unique Next.js (App Router, TypeScript) servant :
- Accueil public : héro, recherche, métiers, promotions, mises à l'honneur
  (données issues de Postgres)
- Backend authentifié avec RBAC : tableau de bord, adhérents (CRUD), modération
  des promotions, administrateurs, espace adhérent (publication avec aperçu)

Technique :
- Drizzle ORM + drizzle-kit (migrations versionnées) sur driver pg
- Auth.js v5 (credentials, sessions JWT), rôles admin/moderator/editor/member
- Middleware de protection de /backend + gardes par rôle dans chaque vue
- Server actions pour toutes les mutations (vérif auth + capacité + revalidate)
- Design fidèle aux maquettes (design-system CSS, animations, responsive)

Conteneurisation :
- 1 conteneur applicatif (sortie standalone) + 1 conteneur Postgres externe
- docker-compose avec healthcheck ; migrations + seed au démarrage
- scripts migrate/seed bundlés (esbuild) pour un runtime sans devDeps

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XwfhYHzC9hQWPDnQ2p53GL
2026-06-26 12:06:21 +00:00