L'annuaire, les pages métier et la fiche adhérent montrent désormais la
personne à joindre chez l'adhérent — nom, prénom, ligne directe — mais
uniquement à un visiteur connecté.
Trois colonnes sur `members` (`contact_first_name`, `contact_last_name`,
`contact_phone`), saisies dans les deux formulaires de fiche via le
composant partagé `MemberContactFields`.
Le filtrage ne repose pas sur du CSS ni sur un rendu conditionnel tardif :
aucune requête publique ne lit ces colonnes. Seules `getMemberContacts()`
et `getMemberContact()` les rapatrient, et les pages ne les appellent
qu'après un `getSession()` positif. Un visiteur anonyme ne reçoit donc
rien — ni dans le HTML, ni dans le JSON-LD, ni dans les props du composant
client de l'annuaire. `tests/security.test.ts` verrouille cette séparation.
`src/lib/member-contact.ts` reste pur (composition du nom, lien `tel:`) et
est couvert par `tests/member-contact.test.ts`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012TAK3c4jAUqxVMzy746wWQ
L'application publie le fichier tel quel sur Facebook et LinkedIn : aucun
recadrage, aucune marge. Un visuel qui n'est pas au format attendu est donc
recadré — ou entouré de bandes de couleur prises dans l'image — par la
plateforme, ce qui ne se voit nulle part côté site puisque les cartes
recadrent en `cover`.
Le formulaire de dépôt affiche maintenant les dimensions, le format et le
poids du fichier choisi, et dit ce que les réseaux en feront : format non
carré, image trop petite pour un fil d'actualité, fichier trop lourd pour
l'envoi. Purement informatif, rien n'est bloqué.
`src/lib/image-info.ts` est pur et verrouillé par `tests/image-info.test.ts`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MS61rkgTUGgPYH88tuVx9U
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
`/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
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
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
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
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
- 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
- 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
- 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
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
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
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
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
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
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
Replace cover-crop background with PromoImage component: sharp
contain image over a blurred cover copy. Whole image visible on
any aspect ratio, no flat letterbox bars. Applied to accueil,
adherent profile, backend promotions, member space preview +
upload dropzone.
Also retheme adherent logo frame (cream gradient + warm border)
instead of flat white.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Avatar logo de la fiche adherent : <img> object-fit contain centre,
cadre a overflow hidden -> plus de debordement quel que soit le ratio
- ImageField : prop fit (cover|contain). Champs logo en contain (apercu
centre, non etire) dans l'edition fiche et l'espace adherent
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Constante BASSIN_POMPEY_COMMUNES (13 communes) dans src/lib/communes.ts
- Champ commune devenu un select dans : ajout adherent, edition fiche,
espace adherent. communeOptions() conserve une valeur existante hors liste.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comptes de connexion adherent
- addMember cree desormais un compte users (role member, lie a la fiche)
avec un mot de passe temporaire genere
- Le mdp temporaire s'affiche sur la fiche admin (/backend/adherents/[id])
jusqu'a la 1ere connexion ; bouton "Reinitialiser le mot de passe"
- 1ere connexion : changement de mot de passe force (must_change_password)
via /backend/changer-mot-de-passe, redirection geree dans authorized()
- users += must_change_password, temp_password (migration 0003)
Boite de reception admin (consultation)
- Page /backend/demandes : demandes d'adhesion + messages de contact
avec statuts (approuvee/rejetee, lu/archive)
- Lien sidebar + badge (nouveaux), carte tableau de bord cliquable
Espace adherent
- L'adherent peut editer sa propre fiche (updateOwnProfile) : infos,
description, horaires, tags, image d'entete + logo
- Statut/categorie/mise a l'honneur restent reserves au staff
Promotions : moderation deja en place, inchangee.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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