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
MonEspaceActorFeedouWallEngine - La disposition 3-colonnes (desktop) / 1-colonne (mobile)
- Quel contexte passer à WallEngine (
- Ce qu'il délègue :
- Détection des villages →
useMonEspaceVillages - Construction du contexte personnel →
usePersonalContext - Gestion du contexte actif →
useMonEspaceActiveContext - Feed proprement dit →
WallEngineouMonEspaceActorFeed - Sidebar →
MonEspaceContexts - Résumé du jour →
MonEspaceTodaySummary
- Détection des villages →
- 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 viacreatePersonalContext() - 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 :
initialPubFilterselon levariant("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=initialPubFilterpassé parWallEngine) - 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
pubFiltersuivisPubs: 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(localStoragedmv_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}/collectesviaNEXT_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
pubSearchrenseigné - Transformation publications →
AgendaEventpour timeline - Groupement par jour, calcul de la target de scroll
- Quel sous-ensemble de publications afficher selon
- 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)
- Appel de tous les sélecteurs (
- 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+WallActorsContainervariant="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"→ affichesuivisPubs(avec avertissements connexion/no follow) - Si
pubFilter === "evenement"→ afficheAgendaFeedSection(buckets temporels) - Sinon → boucle sur
filteredPubs, affichePublicationCard - Placement du sentinel de pagination
- Si
- 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), cachedmv_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 deWallContext→ 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 defollowedIds/lastSeendans ce contexte)
7. Hooks et sélecteurs impliqués
Hooks Mon Espace (couche supérieure)
| Hook | Fichier | Rôle |
|---|---|---|
useMonEspaceVillages(userId) | hooks/useMonEspaceVillages.ts | Villages et commune active |
usePersonalContext({...}) | hooks/usePersonalContext.ts | Contexte personnel + favoris |
useMonEspaceActiveContext({...}) | hooks/useMonEspaceActiveContext.ts | Contexte actif (personal/commune/actor) |
Hooks WallProvider (couche WallEngine)
| Hook | Fichier | Rôle |
|---|---|---|
useWallState() | hooks/useWallState.ts | État UI : pubFilter, drawer, search |
usePublications(communeId) | hooks/usePublications.ts | Publications brutes commune |
useCollectes(communeId) | hooks/useCollectes.ts | Collectes récurrentes |
useFeaturedPublications(communeId) | hooks/useFeaturedPublications.ts | Publications en avant |
useWallActorsData({communeId, communeSlug}) | hooks/useWallActorsData.ts | Acteurs + en avant |
useWallUserSignals({communeId, communeSlug}) | hooks/useWallUserSignals.ts | followedIds, alertes, lastSeen |
useWallTargetedPublications({...}) | hooks/useWallTargetedPublications.ts | suivisPubs / actuPubs / evenementPubs |
useWallAgendaInterests() | hooks/useWallAgendaInterests.ts | "Ça m'intéresse" localStorage |
Sélecteurs
| Sélecteur | Fichier | Rôle |
|---|---|---|
selectPublicationFeed({pubs, pubFilter, suivisPubs, actuPubs, pubSearch, featuredIds}) | selectors/publicationFeedSelectors.ts | Feed filtré principal |
selectPublicationAgendaEvents(pubs) | selectors/publicationFeedSelectors.ts | Publications → AgendaEvent |
selectPublicationTimelineGroups(events, now) | selectors/publicationFeedSelectors.ts | Groupement par jour |
selectPublicationTimelineTargetKey(groups, now) | selectors/publicationFeedSelectors.ts | Scroll target (aujourd'hui) |
selectAgendaEvents(publicationEvents, recurrentEvents) | selectors/agendaSelectors.ts | Merge événements |
selectAgendaEventBuckets(events, now) | selectors/agendaSelectors.ts | future/today/past |
selectAgendaTimelineGroups(buckets, now) | selectors/agendaSelectors.ts | Groupement par jour |
selectAgendaTimelineTargetKey() | selectors/agendaSelectors.ts | Scroll target agenda |
selectInterestedAgendaEvents() | selectors/agendaSelectors.ts | Filtre "Ça m'intéresse" |
Hook spécial (sidebar)
| Hook | Fichier | Rôle |
|---|---|---|
useInfosImportantes(communeId) | MonEspaceContexts.tsx (inline) | 2 prochains événements (14 jours) |
8. Logique métier dispersée
Filtrage
Où : publicationFeedSelectors.ts → selectPublicationFeed()
Logique :
pubFilter = "suivis"→list = suivisPubs(chargé séparément paruseWallTargetedPublications)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
Où : 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
Où : publicationFeedSelectors.ts → selectPublicationFeed()
Logique : Déduplication par contenu (champ exact non documenté dans l'analyse — à confirmer par lecture du code source).
Groupement agenda
Où : agendaSelectors.ts → selectAgendaEventBuckets() + selectAgendaTimelineGroups()
Logique : Segmentation en futureAgendaEvents, todayAgendaEvents, pastAgendaEvents, puis groupement par jour.
Rendu : AgendaFeedSection dans WallFeedContainer (affiché uniquement quand pubFilter = "evenement").
Pagination
Où : usePublications.ts → loadMore() + sentinel dans WallFeedContainer
Mécanisme :
- Curseur :
publie_ledu dernier item chargé - Sentinel
publicationsSentinelRefau bas de la liste IntersectionObserverdansuseWallMurVilleRuntime(non localisé précisément — à confirmer)loadMore()fetch Supabase aveclt("publie_le", cursor)- Pagination distincte pour
actuPubs:loadMoreActuPubs()dansuseWallTargetedPublications - Flux actor : pas de pagination (LIMIT 20 statique)
Ciblage acteurs suivis
Où : Réparti sur trois couches :
usePersonalContext→fetchFavoriteActors()→ liste des acteurs favoris (RPC Supabaselist_favoris_with_latest)useWallUserSignals→followedIds(IDs extraits des favoris)useWallTargetedPublications→suivisPubs(publications desfollowedIds, 7 jours)
Fallback commune
Où : 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
| Fichier | Raison |
|---|---|
WallContext.tsx | Source de vérité centrale, très couplée aux 8 hooks |
usePublications.ts | Gère le cache et la pagination — sensible |
useWallTargetedPublications.ts | Logique de ciblage suivis/actu — risque de régression feed |
publicationFeedSelectors.ts | Logique de filtrage/déduplication utilisée par tous les modes |
WallFeedContainer.tsx | Rendu principal — 405 lignes, nombreux cas de rendu |
WallPublicationCard.tsx | Composant feuille — ne devrait pas évoluer au début |
Fichiers candidats à extraction
| Fichier | Extraction 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 |