Aller au contenu principal

Flow actuel du feed Mon Espace

Statut : Audit de l'état provisoire — lecture seule, aucune modification du code applicatif
Date : 2026-07-06
Portée : dmv-public uniquement
Architecture cible : 34-ranking-engine-synthesis.md — ce document décrit l'état en place ; le doc 34 définit vers quoi migrer


1. Résumé

Le feed central de Mon Espace est construit en deux étages. Le premier étage (MonEspaceShell) orchestre les hooks de contexte utilisateur (villages, acteurs suivis, contexte actif) et choisit entre trois modes : personal, commune, ou actor. Pour les deux premiers modes, il délègue entièrement à WallEngine, qui instancie un WallProvider (8 hooks Supabase en parallèle) et un WallLayout qui exécute les sélecteurs. Pour le mode actor, MonEspaceShell rend directement MonEspaceActorFeed, un composant autonome qui bypasse complètement WallEngine et interroge Supabase directement. Le filtrage (suivis / actu / événement / tout) est piloté par un pubFilter géré dans useWallState, appliqué via selectPublicationFeed dans WallLayout, et finalement consommé par WallFeedContainer pour le rendu des PublicationCard.


2. Schéma du flux

mon-espace/page.tsx
└── MonEspaceShell.tsx
├── [hooks d'initialisation]
│ ├── useMonEspaceVillages() → villages, commune active
│ ├── usePersonalContext() → sources personnelles (commune + favoris)
│ └── useMonEspaceActiveContext() → { activeContextKind, wallCommune }

├── [si activeContextKind === "actor"]
│ └── MonEspaceActorFeed.tsx → fetch Supabase direct → PublicationCard

└── [si activeContextKind === "personal" | "commune"]
└── WallEngine.tsx (variant="embedded")
└── WallProvider (WallContext.tsx)
├── useWallState()
├── usePublications()
├── useCollectes()
├── useFeaturedPublications()
├── useWallActorsData()
├── useWallUserSignals()
├── useWallTargetedPublications()
└── useWallAgendaInterests()
└── WallLayout.tsx
├── selectPublicationFeed()
├── selectAgendaEvents()
└── WallMurVilleHostContainer.tsx
└── WallFeedContainer.tsx
└── PublicationCard (×N)

3. Fichiers exacts

dmv-public/app/mon-espace/page.tsx

  • Rôle : Point d'entrée de la route /mon-espace
  • Ce qu'il décide : Rien — simple wrapper
  • Ce qu'il délègue : Tout à MonEspaceShell
  • Logique métier : Non

dmv-public/app/components/mon-espace/MonEspaceShell.tsx

  • Rôle : Orchestrateur principal de Mon Espace (~490 lignes)
  • Ce qu'il décide :
    • Quel contexte passer à WallEngine (WallContextDefinition)
    • Si on rend MonEspaceActorFeed ou WallEngine
    • La disposition 3-colonnes (desktop) / 1-colonne (mobile)
  • Ce qu'il délègue :
    • Détection des villages → useMonEspaceVillages
    • Construction du contexte personnel → usePersonalContext
    • Gestion du contexte actif → useMonEspaceActiveContext
    • Feed proprement dit → WallEngine ou MonEspaceActorFeed
    • Sidebar → MonEspaceContexts
    • Résumé du jour → MonEspaceTodaySummary
  • Logique métier : Oui — arbitre le mode d'affichage (personal/commune/actor)

dmv-public/app/components/mon-espace/hooks/useMonEspaceVillages.ts

  • Rôle : Déterminer les villages/communes disponibles pour l'utilisateur
  • Ce qu'il décide : Status de disponibilité (loading/ready/missing/error), liste des villages, commune active
  • Ce qu'il délègue : Fetch Supabase (profil, acteurs gérés, favoris, contributor)
  • Logique métier : Oui — agrège plusieurs sources pour constituer la liste de communes

dmv-public/app/components/mon-espace/hooks/usePersonalContext.ts

  • Rôle : Construire le PersonalContext (commune + acteurs suivis)
  • Ce qu'il décide : Quels acteurs sont des favoris, met en cache localStorage (dmv_espace_fav_v1:{contributorId})
  • Ce qu'il délègue : RPC Supabase list_favoris_with_latest, construction via createPersonalContext()
  • Logique métier : Oui — définit les sources du contexte personnel

dmv-public/app/components/mon-espace/hooks/useMonEspaceActiveContext.ts

  • Rôle : Gérer le contexte actif sélectionné par l'utilisateur
  • Ce qu'il décide : activeContextKind (personal | commune | actor), wallCommune, currentContextLabel
  • Ce qu'il délègue : Rien (pure state machine)
  • Logique métier : Oui — point de décision du basculement entre les trois modes

dmv-public/app/components/wall/contexts/personal/createPersonalContext.ts

  • Rôle : Factory qui construit un objet PersonalContext
  • Ce qu'il décide : Structure des sources (PersonalPrimaryCommuneSource, PersonalFavoriteActorsSource)
  • Ce qu'il délègue : Rien
  • Logique métier : Oui — définit le type et la forme du contexte personnel

dmv-public/app/components/wall/contexts/types.ts

  • Rôle : Définition des types WallContextDefinition (commune | personal | actor)
  • Ce qu'il décide : Rien (types uniquement)
  • Ce qu'il délègue : Rien
  • Logique métier : Non — types TypeScript

dmv-public/app/components/wall/WallEngine.tsx

  • Rôle : Point d'entrée du WallEngine, instancie providers
  • Ce qu'il décide : initialPubFilter selon le variant ("embedded""suivis", "standalone""all")
  • Ce qu'il délègue : Rendu et données → WallProvider + WallLayout
  • Logique métier : Oui (faible) — choix du filtre initial selon le contexte d'usage

dmv-public/app/components/wall/context/WallContext.tsx

  • Rôle : Provider React central, source de vérité partagée pour tout le WallEngine
  • Ce qu'il décide : Quels hooks appeler, comment agréger leurs valeurs dans WallContextValue
  • Ce qu'il délègue : Chaque domaine de données à son hook dédié (8 hooks)
  • Logique métier : Faible — coordination, pas de calcul métier

dmv-public/app/components/wall/hooks/useWallState.ts

  • Rôle : État UI du wall (filtre actif, drawer, searchs, panels)
  • Ce qu'il décide : Valeurs initiales (dont pubFilter = initialPubFilter passé par WallEngine)
  • Ce qu'il délègue : Rien
  • Logique métier : Non — pure gestion d'état UI

dmv-public/app/components/wall/hooks/usePublications.ts

  • Rôle : Charger les publications récentes d'une commune (~188 lignes)
  • Ce qu'il décide :
    • Fenêtre temporelle (24h en cache, fallback batch plus ancien)
    • Filtres fixes : valide=true, moderation_status IN (approved, published), acteur_id IS NOT NULL, contributor_id IS NULL
    • Limite : 100 items
    • Cache localStorage : dmv_wall_publications_{communeId}
  • Ce qu'il délègue : Rien
  • Logique métier : Oui — définit la fenêtre de données brutes et les critères de validité

dmv-public/app/components/wall/hooks/useWallTargetedPublications.ts

  • Rôle : Charger les publications spécialisées (suivis / actu / événements) (~100 lignes)
  • Ce qu'il décide : Trois flux parallèles selon pubFilter
    • suivisPubs : Publications des acteurs suivis (7 derniers jours)
    • actuPubs : Publications de type actualité (type_id = TYPE_ACTUALITE)
    • evenementPubs : Publications de type événement
  • Ce qu'il délègue : Rien
  • Logique métier : Oui — ciblage des publications selon les acteurs suivis

dmv-public/app/components/wall/hooks/useWallUserSignals.ts

  • Rôle : Signaux utilisateur (connexion, suivi, alertes, lastSeen)
  • Ce qu'il décide : followedIds, favorisAlertes, mairieAlertes, lastSeen (localStorage dmv_last_seen_{slug})
  • Ce qu'il délègue : Rien
  • Logique métier : Oui — détermine quels acteurs sont suivis (entrée de suivisPubs)

dmv-public/app/components/wall/hooks/useWallActorsData.ts

  • Rôle : Données des acteurs (répertoire + en avant)
  • Ce qu'il décide : acteurs[], acteursEnAvant[]
  • Ce qu'il délègue : Fetch Supabase (acteurs, tags, catégories, badges)
  • Logique métier : Non — fetch pur

dmv-public/app/components/wall/hooks/useFeaturedPublications.ts

  • Rôle : Charger les 5 publications en avant (en_avant = true)
  • Ce qu'il décide : Plafond à 5, cache dmv_wall_featured_v1:{communeId}
  • Ce qu'il délègue : Rien
  • Logique métier : Non — fetch filtré

dmv-public/app/components/wall/hooks/useCollectes.ts

  • Rôle : Charger les collectes récurrentes
  • Ce qu'il décide : Appel REST (/communes/{communeId}/collectes via NEXT_PUBLIC_API_URL)
  • Ce qu'il délègue : Rien
  • Logique métier : Non — fetch pur

dmv-public/app/components/wall/hooks/useWallAgendaInterests.ts

  • Rôle : Gérer les intérêts agenda ("Ça m'intéresse") en localStorage
  • Ce qu'il décide : Clé dmv_agenda_interests (Set d'IDs)
  • Ce qu'il délègue : Rien
  • Logique métier : Non — persistance locale

dmv-public/app/components/wall/selectors/publicationFeedSelectors.ts

  • Rôle : Filtrage et transformation du feed (~122 lignes)
  • Ce qu'il décide :
    • Quel sous-ensemble de publications afficher selon pubFilter
    • Déduplication par contenu
    • Filtrage texte si pubSearch renseigné
    • Transformation publications → AgendaEvent pour timeline
    • Groupement par jour, calcul de la target de scroll
  • Ce qu'il délègue : Rien
  • Logique métier : Oui — cœur du filtrage et de la présentation

dmv-public/app/components/wall/selectors/agendaSelectors.ts

  • Rôle : Groupement et filtrage des événements (~84 lignes)
  • Ce qu'il décide : futureAgendaEvents, todayAgendaEvents, pastAgendaEvents, groupement par jour
  • Ce qu'il délègue : Rien
  • Logique métier : Oui — logique de segmentation temporelle

dmv-public/app/components/wall/components/WallLayout.tsx

  • Rôle : Orchestration complète du rendu (~463 lignes)
  • Ce qu'il décide :
    • Appel de tous les sélecteurs (selectPublicationFeed, selectAgendaEvents, etc.)
    • Construction des props pour WallMurVilleHostContainer
    • Gestion des interactions (filtres, pagination, recherche)
  • Ce qu'il délègue : Rendu visuel → WallMurVilleHostContainer
  • Logique métier : Oui — agrège données + sélecteurs + interactions

dmv-public/app/components/wall/components/mur-ville/WallMurVilleHostContainer.tsx

  • Rôle : Conteneur final qui compose les sous-conteneurs selon le variant (~105 lignes)
  • Ce qu'il décide :
    • variant="embedded" (Mon Espace) : WallFeedContainer + WallActorsContainer
    • variant="standalone" (Mur public) : WallShellContainer + WallFeedContainer + WallActorsContainer + WallSecondaryPagesContainer
  • Ce qu'il délègue : Chaque section à son conteneur dédié
  • Logique métier : Non — routing de layout

dmv-public/app/components/wall/components/feed/WallFeedContainer.tsx

  • Rôle : Rendu du feed d'actualités et événements (~405 lignes)
  • Ce qu'il décide :
    • Si pubFilter === "suivis" → affiche suivisPubs (avec avertissements connexion/no follow)
    • Si pubFilter === "evenement" → affiche AgendaFeedSection (buckets temporels)
    • Sinon → boucle sur filteredPubs, affiche PublicationCard
    • Placement du sentinel de pagination
  • Ce qu'il délègue : Rendu carte → PublicationCard
  • Logique métier : Faible — branchement sur pubFilter, reste rendu

dmv-public/app/components/wall/components/publications/WallPublicationCard.tsx

  • Rôle : Carte individuelle d'une publication
  • Ce qu'il décide : Affichage badge "Nouveauté" (isNew), badge "Suivi" (isFavori), variant condensed/expanded
  • Ce qu'il délègue : Rien
  • Logique métier : Non — rendu pur

dmv-public/app/components/mon-espace/MonEspaceActorFeed.tsx

  • Rôle : Feed autonome pour un acteur sélectionné (~151 lignes)
  • Ce qu'il décide : Fetch direct Supabase (publications WHERE acteur_id = actor.id LIMIT 20), cache dmv_actor_pubs_v1:{actor.id}
  • Ce qu'il délègue : Rendu → PublicationCard
  • Logique métier : Oui — bypass complet de WallEngine, fetch propriétaire

dmv-public/app/components/mon-espace/MonEspaceContexts.tsx

  • Rôle : Sidebar droite de Mon Espace (~469 lignes)
  • Ce qu'il décide : Affichage des 3 premiers acteurs favoris, liens agenda, infos importantes
  • Ce qu'il délègue : Hook interne useInfosImportantes(communeId) (2 prochains événements sur 14 jours)
  • Logique métier : Faible — présentation des données déjà calculées

dmv-public/app/components/mon-espace/MonEspaceTodaySummary.tsx

  • Rôle : Résumé en haut du feed avec compteurs (événements aujourd'hui, suivis, alertes)
  • Ce qu'il décide : Affichage conditionnel des badges
  • Ce qu'il délègue : Données comptées depuis WallContext
  • Logique métier : Non — rendu pur

4. Flux personal

useMonEspaceActiveContext → activeContextKind = "personal"

MonEspaceShell construit WallContextDefinition :
{
kind: "personal",
commune: activeCommune, // commune du profil utilisateur
communeIds: [activeCommune.id],
label: "Pour moi",
sources: [
PersonalPrimaryCommuneSource, // commune principale
PersonalFavoriteActorsSource // acteurs suivis
]
}

WallEngine (variant="embedded")
→ initialPubFilter = "suivis" // filtre initial en mode embedded

WallProvider
→ useWallUserSignals → followedIds // IDs des acteurs du PersonalContext.sources
→ useWallTargetedPublications → suivisPubs (publications des followedIds, 7 jours)
→ usePublications(communeId) // publications brutes de la commune

WallLayout
→ selectPublicationFeed(pubFilter="suivis") → list = suivisPubs

WallFeedContainer
→ pubFilter === "suivis" → affiche suivisPubs
→ si non connecté ou 0 favoris → bannière d'avertissement

PublicationCard (×N)

Note : En mode personal, le feed démarre sur les publications des acteurs suivis (pubFilter = "suivis"). Les publications brutes de la commune sont chargées en arrière-plan mais n'apparaissent que si l'utilisateur change le filtre.


5. Flux commune

useMonEspaceActiveContext → activeContextKind = "commune", wallCommune = communeChoisie

MonEspaceShell construit WallContextDefinition :
{
kind: "commune",
commune: wallCommune,
communeIds: [wallCommune.id],
label: wallCommune.nom
}

WallEngine (variant="embedded")
→ initialPubFilter = "suivis" // toujours "suivis" en embedded

WallProvider
→ usePublications(wallCommune.id) // toutes publications de la commune choisie
→ useWallUserSignals → followedIds
→ useWallTargetedPublications → suivisPubs/actuPubs/evenementPubs pour wallCommune

WallLayout
→ sélecteurs appliqués sur les données de wallCommune

WallFeedContainer → WallPublicationCard

Différence avec personal : Le WallContextDefinition.kind est "commune" (pas de sources). La commune active change, mais le WallProvider traite les deux de manière quasi-identique — communeId est extrait de WallContextDefinition.commune.id dans les deux cas.


6. Flux actor

Le flux acteur est entièrement distinct et bypass WallEngine.

useMonEspaceActiveContext → activeContextKind = "actor", activeActor = acteurSélectionné

MonEspaceShell
→ NE crée PAS de WallContextDefinition
→ NE rend PAS WallEngine

MonEspaceActorFeed.tsx
→ fetch direct Supabase :
SELECT * FROM publications
WHERE acteur_id = actor.id
AND valide = true
AND deleted_at IS NULL
ORDER BY publie_le DESC
LIMIT 20
→ cache localStorage : dmv_actor_pubs_v1:{actor.id}

PublicationCard (×N, sans contexte WallEngine)

Implications :

  • Pas de WallProvider → pas de WallContext → pas d'accès aux sélecteurs / signaux / pagination infinie
  • Pas de pubFilter → tout s'affiche, non filtrable
  • Pagination : statique (LIMIT 20), pas de loadMore
  • isNew / isFavori : non disponibles (pas de followedIds / lastSeen dans ce contexte)

7. Hooks et sélecteurs impliqués

Hooks Mon Espace (couche supérieure)

HookFichierRôle
useMonEspaceVillages(userId)hooks/useMonEspaceVillages.tsVillages et commune active
usePersonalContext({...})hooks/usePersonalContext.tsContexte personnel + favoris
useMonEspaceActiveContext({...})hooks/useMonEspaceActiveContext.tsContexte actif (personal/commune/actor)

Hooks WallProvider (couche WallEngine)

HookFichierRôle
useWallState()hooks/useWallState.tsÉtat UI : pubFilter, drawer, search
usePublications(communeId)hooks/usePublications.tsPublications brutes commune
useCollectes(communeId)hooks/useCollectes.tsCollectes récurrentes
useFeaturedPublications(communeId)hooks/useFeaturedPublications.tsPublications en avant
useWallActorsData({communeId, communeSlug})hooks/useWallActorsData.tsActeurs + en avant
useWallUserSignals({communeId, communeSlug})hooks/useWallUserSignals.tsfollowedIds, alertes, lastSeen
useWallTargetedPublications({...})hooks/useWallTargetedPublications.tssuivisPubs / actuPubs / evenementPubs
useWallAgendaInterests()hooks/useWallAgendaInterests.ts"Ça m'intéresse" localStorage

Sélecteurs

SélecteurFichierRôle
selectPublicationFeed({pubs, pubFilter, suivisPubs, actuPubs, pubSearch, featuredIds})selectors/publicationFeedSelectors.tsFeed filtré principal
selectPublicationAgendaEvents(pubs)selectors/publicationFeedSelectors.tsPublications → AgendaEvent
selectPublicationTimelineGroups(events, now)selectors/publicationFeedSelectors.tsGroupement par jour
selectPublicationTimelineTargetKey(groups, now)selectors/publicationFeedSelectors.tsScroll target (aujourd'hui)
selectAgendaEvents(publicationEvents, recurrentEvents)selectors/agendaSelectors.tsMerge événements
selectAgendaEventBuckets(events, now)selectors/agendaSelectors.tsfuture/today/past
selectAgendaTimelineGroups(buckets, now)selectors/agendaSelectors.tsGroupement par jour
selectAgendaTimelineTargetKey()selectors/agendaSelectors.tsScroll target agenda
selectInterestedAgendaEvents()selectors/agendaSelectors.tsFiltre "Ça m'intéresse"

Hook spécial (sidebar)

HookFichierRôle
useInfosImportantes(communeId)MonEspaceContexts.tsx (inline)2 prochains événements (14 jours)

8. Logique métier dispersée

Filtrage

: publicationFeedSelectors.tsselectPublicationFeed()
Logique :

  • pubFilter = "suivis"list = suivisPubs (chargé séparément par useWallTargetedPublications)
  • pubFilter = "actu"list = actuPubs (idem)
  • pubFilter = "evenement"list = pubs.filter(type === "evenement")
  • pubFilter = "all"list = pubs.filter(NOT featured)

Secondaire : WallFeedContainer branche le rendu selon pubFilter (liste vs AgendaFeedSection).


Priorisation

: Aucun sélecteur de priorisation explicite trouvé.
L'ordre est celui de Supabase : ORDER BY publie_le DESC. Les publications en avant (featuredIds) sont exclues du feed principal (filtre NOT featured) et affichées séparément via useFeaturedPublications.


Déduplication

: publicationFeedSelectors.tsselectPublicationFeed()
Logique : Déduplication par contenu (champ exact non documenté dans l'analyse — à confirmer par lecture du code source).


Groupement agenda

: agendaSelectors.tsselectAgendaEventBuckets() + selectAgendaTimelineGroups()
Logique : Segmentation en futureAgendaEvents, todayAgendaEvents, pastAgendaEvents, puis groupement par jour.
Rendu : AgendaFeedSection dans WallFeedContainer (affiché uniquement quand pubFilter = "evenement").


Pagination

: usePublications.tsloadMore() + sentinel dans WallFeedContainer
Mécanisme :

  • Curseur : publie_le du dernier item chargé
  • Sentinel publicationsSentinelRef au bas de la liste
  • IntersectionObserver dans useWallMurVilleRuntime (non localisé précisément — à confirmer)
  • loadMore() fetch Supabase avec lt("publie_le", cursor)
  • Pagination distincte pour actuPubs : loadMoreActuPubs() dans useWallTargetedPublications
  • Flux actor : pas de pagination (LIMIT 20 statique)

Ciblage acteurs suivis

: Réparti sur trois couches :

  1. usePersonalContextfetchFavoriteActors() → liste des acteurs favoris (RPC Supabase list_favoris_with_latest)
  2. useWallUserSignalsfollowedIds (IDs extraits des favoris)
  3. useWallTargetedPublicationssuivisPubs (publications des followedIds, 7 jours)

Fallback commune

: useMonEspaceVillages.ts
Logique : Si l'utilisateur n'a pas de commune définie, fallback sur la première commune disponible (profil, acteurs gérés, etc.). Si aucune commune trouvée → status "missing".


9. Conclusion

Meilleur point d'entrée pour migrer vers le Context Engine

MonEspaceShell.tsx est le meilleur point d'entrée.

C'est là que se prend la décision activeContextKind (personal/commune/actor) et que le WallContextDefinition est construit. Remplacer la construction de WallContextDefinition par un appel au Context Engine (qui déciderait lui-même du contexte à partir du profil utilisateur et des signaux) serait chirurgical et n'impliquerait pas de toucher à WallEngine ou aux hooks de données.


Fichiers à ne pas toucher au début

FichierRaison
WallContext.tsxSource de vérité centrale, très couplée aux 8 hooks
usePublications.tsGère le cache et la pagination — sensible
useWallTargetedPublications.tsLogique de ciblage suivis/actu — risque de régression feed
publicationFeedSelectors.tsLogique de filtrage/déduplication utilisée par tous les modes
WallFeedContainer.tsxRendu principal — 405 lignes, nombreux cas de rendu
WallPublicationCard.tsxComposant feuille — ne devrait pas évoluer au début

Fichiers candidats à extraction

FichierExtraction suggérée
useMonEspaceActiveContext.ts→ Remplacer par un useContextEngine() qui retourne le même contrat (activeContextKind, wallCommune, etc.)
usePersonalContext.ts→ Intégrer la logique de favoris dans le Context Engine
useMonEspaceVillages.ts→ Devenir une source d'input du Context Engine
MonEspaceActorFeed.tsx→ Candidat à unification : passer par WallEngine avec kind="actor" plutôt qu'un fetch propriétaire
createPersonalContext.ts→ Déplacer dans le Context Engine comme fonction de construction