Repository navigation
Exposer GET /routes/:id et /routes/:id/timeline, stocker computed #36
Description
Contexte
Epic : #9
Maquette : sans objet — API ; la fiche E-05 qu'elle alimente est validée depuis le 2026-09-23
API › Fiche parcours · API › Choix tranchés (#393) · Modele-de-Donnees (routes.unpublished_at) · Orchestration · Ecrans › E-05 · E-05 · D-002 · D-088 · D-093
Débloqué le 2026-10-14 par D-088 (décision d'Ilan sur #378) : computed et steps.transition n'enregistrent que nos estimations ; à chaque GET /v1/routes/:id, l'API demande à Mapbox Directions les trajets exacts et leur géométrie pour cette réponse seulement, sans rien enregistrer ni mettre en cache (direction_cache reste vide) ; si Mapbox ne répond pas, la réponse porte l'estimation (estimated: true), sans géométrie.
Contrat écrit le 2026-10-16 par D-093 (décision d'Ilan sur #393) : la section API › Fiche parcours fait foi. Point de départ : le schéma RouteDetail de packages/shared/src/schemas/route-detail.ts (PR #390, #37), étendu par #508 (highlights, photos des étapes complètes) ; l'endpoint tient ce schéma partagé, sans seconde définition.
Comportement attendu
Accès (tableau « Qui obtient quoi » de la page API) :
published,needs_fix: 200 pour tous.private: 404 sans jeton, 200 avec?token=valide (sharedBy).pendingouchanges_requestedjamais publié : 200 avec le jeton (sharedBy), 404 sans.pendingouchanges_requesteddéjà publié (unneeds_fixcorrigé et resoumis) : 404, même avec le jeton.draft,rejected: 404. Inexistant ou supprimé avec le compte de son auteur : 404. Jeton faux ou d'un autre parcours : 404, jamais 403. Identifiant mal formé : 400validation_error. Jeton Firebase invalide : 401.- L'auteur connecté a toujours droit à son propre parcours (200 sur ses parcours
private,pending,changes_requested, et parcours Premium en entier, fiche et programmation) :canAccessRoute(route, viewer, now)(apps/api/src/access/route-access.ts) est étendu à l'auteur, règle unique déjà lue par la recherche et la programmation. - Pas d'exception de rôle (un modérateur passe par
GET /admin/routes/:id).
Réponse 200 de GET /v1/routes/:id : RouteDetail tel que décrit dans la page API (identité de la card, status, description, chiffres issus de computed, startAccess, creator, sharedBy, reviews (3 derniers), timeline du départ conseillé, steps complètes ou LockedStep, vérification et lieux signalés), plus ce que le schéma de #390 ne porte pas encore :
geometryde chaque trajet (transition.geometry) : polyline encodée, précision 5 (ce queapps/api/src/directions/mapbox.tsdemande déjà),nullsans tracé ; pour le tracé de Tracer le parcours sélectionné le long des rues (E-04) #154 ;myReport(D-057) : date et statut du signalement de l'utilisateur connecté s'il date de moins de 7 jours et n'est pas tranché,nullsinon ; absent ounullsans jeton ;- « À prévoir » : lignes du créateur (
routes.tips) et suggestions calculées non masquées (computed, moinsroutes.tips_hidden), D-048, D-050, lues par E-06 ; - photos en objets photo
{ url, credit }(le typePhotode Servir « À ne pas manquer » et les photos d'étape avec leur crédit (snapshot du Go, fiche) #508,creditànullpour une photo de créateur) :photoUrlsetplace.photoUrlcessent d'être de simples URL ; timezonede la ville (fuseau IANA, commePlannedRouteSnapshot) : « Ouvert aujourd'hui » se lit à l'heure de Paris.- Aucun
warningsnierrorsde cohérence dans la fiche (Q10).
Trajets : la fiche calcule durée, distance, marche, effort et sa timeline sur les trajets de la réponse (exacts quand Mapbox répond) ; paliers et filtres restent sur l'enregistré (Q6). Profils walking, cycling, driving ; métro toujours estimé, sans tracé. Repli, jamais une erreur : Mapbox en échec, au-delà de 4 s ou quota atteint → estimation enregistrée, estimated: true, geometry: null. Piste : une requête par profil et par parcours, les étapes en points de passage (25 au plus), pour tenir la limite de 300 requêtes par minute.
Query : token, trips=estimated (nos estimations seules, sans appel à Mapbox ni tracé, réponse cachable comme /timeline : lecture du site /r/:id, Q9) ; toute autre clé : 400 validation_error.
Parcours Premium sans droit (isLocked, D-014, D-075) : étape 1 complète, étapes suivantes en LockedStep strict (id, position, category, durationMin, transition) ; aucun appel à Mapbox, trajets estimés, geometry: null partout ; conflits de la timeline vides sur les étapes verrouillées (Q7).
Parcours retiré (unpublished) : 410 route_removed à tout appelant, avec ou sans jeton, details: { removedAt, near } et rien d'autre du parcours (ni titre, ni auteur, ni étapes, ni motif).
removedAt: nouvelle colonneroutes.unpublished_at(timestamptz, migration additive), posée par l'écrivain unique du statut (moveRoute) à chaque passage enunpublished, remise ànullà la republication.near: coordonnées du départ arrondies à 0,01° (deux décimales), jamais le point exact.ApiErroravecapiCodeexplicite (410 n'a pas de code par défaut danscodeByStatus) ;RouteRemovedDetails(packages/shared) gagnenear.
En-têtes et limites :
- Réponse 200 de
GET /routes/:id:Cache-Control: no-store, sans ETag (Q8) ; sauf avectrips=estimated, cachable comme/timeline. - 60 requêtes par minute et par IP sur
GET /routes/:id, 429rate_limitedau-delà. - Erreurs (400, 401, 402, 404, 410, 429) jamais mises en cache.
GET /v1/routes/:id/timeline?startsAt= :
startsAtobligatoire (ISO 8601 avec décalage, commePOST /me/planned),tokenpour un parcours privé ; clé inconnue ou valeur invalide : 400.- Mêmes règles d'accès que la fiche (404, 410, jeton). Parcours Premium sans droit : 402
premium_required,details.reason: premium_route. RouteTimelinepour ce départ (horaires valables à la date demandée, conflits compris), calculée sur nos estimations, celles quePOST /me/plannedfige, sans appel à Mapbox (D-093, qui amende D-088).- Cache :
public(60 s, ETag) sansAuthorizationnitoken;private, no-cacheetVary: Authorizationavec un jeton Firebase ;private, no-storeavec untokende lien privé.
Contenu par étape et pour le parcours (D-013, D-014, D-048, D-050, D-052, D-056) : horaires du jour et statut ouvert, coût, réservation (bookingRequired, bookingUrl, bookingProvider, bookingNote, bookingIsAffiliate), endsOn, transitionNote, conseil du créateur, vérification { confirmations, lastConfirmedAt, reports }, verificationStatus et verifiedAt du lieu, lieux signalés ; nombre d'avis, trois derniers avis, nombre de parcours publiés par le créateur.
Budget (D-032, D-066, Q11) : budgetPerPersonEur est la somme exacte par personne, comme sur la card de /routes/search, et budgetBucket dérive de cette somme ; l'API n'arrondit pas, l'arrondi d'affichage reste la fonction de l'app partagée avec la card.
Recalcul : computed recalculé et stocké à chaque modification d'étapes (service recomputeRoute), sur nos estimations seulement (D-088), remplaçant les valeurs manuelles du seed.
Critères d'acceptation
- Tests d'intégration de l'accès, une ligne par case du tableau « Qui obtient quoi » : publié,
needs_fix, privé avec et sans jeton,pendingjamais publié avec jeton,pendingdéjà publié (404 même avec jeton),draftetrejected, jeton faux (404), identifiant mal formé (400) - Test : l'auteur connecté lit son parcours
privatesans jeton, et son propre parcours Premium en entier sans abonnement ;canAccessRoutereste l'unique règle (recherche, programmation et fiche) - Le seed v0.2 est recalculé par le moteur et reste cohérent
- Test : parcours
access = premiumsans droit → étape 1 complète, étapes suivantes enLockedStep, aucun appel à Mapbox,geometry: nullpartout, conflits vides sur les étapes verrouillées ; avec droit → fiche complète ; comptepremiumdontplan_expires_atest passé → fiche réduite (D-075) - Test : les transitions portent la géométrie demandée à Mapbox (polyline, précision 5) ; rien de Mapbox n'est écrit en base (
computed,steps.transition,direction_cache) ; Mapbox indisponible → estimations,estimated: true, sans géométrie - Test :
?trips=estimated→ aucun appel à Mapbox,geometry: null, réponse cachable ; toute autre clé de query → 400 - Test : parcours retiré → 410
route_removedavecdetails.removedAt(lu dansroutes.unpublished_at) etdetails.neararrondi à 0,01°, sans autre donnée du parcours ;unpublished_atposé parmoveRouteau retrait et remis ànullà la republication - La réponse porte
geometry,myReport, « À prévoir », les photos en objets{ url, credit },timezone, et tous les champs par étape et pour le parcours listés ci-dessus, lieux signalés compris ; aucun avertissement de cohérence - La réponse est validée par le schéma partagé
route-detail.ts, étendu des champs ci-dessus danspackages/shared, tel que décrit dans API › Fiche parcours - Test : réponse 200 en
Cache-Control: no-store, sans ETag ; la 61e requête d'une minute depuis la même IP reçoit 429rate_limited - Test :
/timeline?startsAt=sans appel à Mapbox, sur les mêmes estimations quePOST /me/planned; 402premium_requiredpour un parcours Premium sans droit ; 400 sansstartsAt; en-têtes de cache des trois cas - Test :
budgetPerPersonEurexact (23 € reste 23), égal à celui de la card du même parcours, etbudgetBucketdérivé de la somme exacte ; aucun arrondi dans l'API - Le lien de réservation de chaque étape vient de
resolveBookingLink(packages/shared/src/booking-link.ts) : lien du créateur d'abord, lien partenaire sinon,bookingIsAffiliatevrai pour un lien partenaire (test API)
Notes techniques
- Niveau 2 attendu : migration additive (
routes.unpublished_at), extension d'une règle d'accès (canAccessRoute), limite de débit, appels sortants à Mapbox ; revue OWASP ciblée (A01 accès par statut et jeton, A04 limite de débit et coût des appels tiers). - Hors de ce ticket : la traduction (
route_translations, D-076) ; l'état « déjà programmé » de la barre (D-035), lu surGET /me/planned; la lecture d'un déblocage fait sans compte (question ouverte par D-096, portée par Décider comment la fiche reconnaît un parcours débloqué sans compte #531,needs-design).
Activity
IlanAMG commentedon Sep 25, 2026
D-048 (https://github.com/Inprogress-Agency/widoo-app/wiki/Design-Changelog) : exposer suggested_start, tips (À prévoir), getting_there (Comment venir), et par étape booking_note, booking_is_affiliate et le mode du trajet. Schéma : #176.
IlanAMG commentedon Sep 25, 2026
D-050 (https://github.com/Inprogress-Agency/widoo-app/wiki/Design-Changelog) : exposer computed.access (#180), les suggestions « À prévoir » non masquées, ends_on des lieux et étapes, le crédit des photos.
IlanAMG commentedon Sep 25, 2026
D-052 (https://github.com/Inprogress-Agency/widoo-app/wiki/Design-Changelog) : exposer ends_on par étape, computed.access (#180), booking_is_affiliate, le mode de chaque trajet, et le crédit des médias (author, license, source_url).
IlanAMG commentedon Sep 26, 2026
D-056 (https://github.com/Inprogress-Agency/widoo-app/wiki/Design-Changelog) :
GET /routes/:idexpose par étape la date de la dernière vérification du lieu (verified_at) et le statut, pour la ligne « Dernière vérification il y a N mois » ; aucune donnée de signalement de parcours n'est exposée.IlanAMG commentedon Sep 26, 2026
D-057 (https://github.com/Inprogress-Agency/widoo-app/wiki/Design-Changelog) :
GET /routes/:idexposemyReport(date du signalement de l'utilisateur connecté s'il date de moins de 7 jours et n'est pas encore tranché, sinon null).PaulThiberville commentedon Sep 29, 2026
📋 Report de D-075 (2026-09-29, correction de D-066 sur la card de recherche Premium, décision de Paul au nom d'Ilan ; voir #161) : critère ajouté au corps.
/routes/:idapplique la même règle de droit au parcours que la recherche (fonction unique livrée par #161), sans seconde implémentation. Le critère existant « avec abonnement → fiche complète » reste valable.feat(api): decide the right to a route in one place, per caller
13 remaining items
PaulThiberville commentedon Oct 14, 2026
📋 MANAGER (2026-10-14), découverte de CODE pendant #41 (PR #443, Inbox) : E-06 (« À préparer ») attend les conseils du parcours, que
RouteDetailne porte pas encore. Ils sont déjà dans le périmètre de ce ticket : API › GET /routes/:id, « À ajouter au schéma de #390 par #36 », ligne « À prévoir » (routes.tipset suggestions calculées non masquées, D-048, D-050). Pas de nouveau ticket API. Leur affichage et leur cochage dans la checklist d'une sortie (queplanned_routes.checklistne sait pas garder) partent en #445 (needs-design, bloqué par ce ticket).Au passage : aucune fiche de démo (
apps/mobile/src/route/generated/route-sheets.json) n'a debookingUrlni debookingNote, si bien que « Réserver » et « Lien affilié » ne se voient pas en recette. Le seed recalculé de ce ticket (critère « Le seed v0.2 est recalculé ») est l'occasion d'en donner à au moins une étape de démo (lien fictif).PaulThiberville commentedon Oct 15, 2026
📋 MANAGER, 2026-10-15 — à prendre en compte ici (découverte de CODE pendant #180, PR #482) :
routes.computed.accessest maintenant écrit, sous la forme{ station, lines: [{ mode, line }], distance_m }(ounullau-delà de 1,5 km). La fiche ne le sert pas encore :RouteDetail.startAccessn'existe que dans les fixtures de l'app (startAccess: null), et sonlines: string[]sans mode ne permet pas « Métro Abbesses, ligne 12 » de la maquette (D-052,mappings.access) ni « à 50 m du départ ».GET /routes/:iddevrait servir l'accès avec son mode et sa distance ; l'affichage côté app suivra. La forme decomputed.accessest à reporter dans Modele-de-Donnees (voir #436).PaulThiberville commentedon Oct 16, 2026
📋 MANAGER, 2026-10-16 — deux ajouts au schéma que ce ticket sert :
RouteDetailStep(packages/shared) et au snapshot de/me/planned, avec le crédit des photos libres (D-052). Si Exposer GET /routes/:id et /routes/:id/timeline, stocker computed #36 est fait avant,GET /v1/routes/:idles servira dès que Servir « À ne pas manquer » et les photos d'étape avec leur crédit (snapshot du Go, fiche) #508 aura étendu le schéma ; si Servir « À ne pas manquer » et les photos d'étape avec leur crédit (snapshot du Go, fiche) #508 est fait avant, Exposer GET /routes/:id et /routes/:id/timeline, stocker computed #36 les reprend sans seconde définition. Découverte de CODE sur Construire le parcours en cours et le lien Google Maps (E-07) #42 : Inbox.resolveBookingLink(packages/shared/src/booking-link.ts) avec le lieu et son programme (places.partner_booking_url,affiliate_programs.is_active) : lien du créateur d'abord, partenaire sinon,bookingIsAffiliateà vrai pour un lien partenaire. Sans cela, aucun lien partenaire de Widoo ne s'affiche dans l'app. Critère ajouté ci-dessous.IlanAMG commentedon Oct 16, 2026
Chef de projet (16/10/2026) : glissé du Sprint 0 au Sprint 1, non terminé.
IlanAMG commentedon Oct 16, 2026
Chef de projet (16/10/2026) : jalon v0.3 → v0.4, décision d'Ilan du 16/10 (v0.3 échoit le 17/10 sans ce ticket).
PaulThiberville commentedon Oct 19, 2026
📋 MANAGER, 2026-10-19 — report de D-093 (décision d'Ilan sur #393) : le contrat de API › Fiche parcours est reporté dans le corps.
canAccessRouteétendu à l'auteur ; au schéma de feat(mobile): construire la fiche parcours (E-05) #390,geometry(polyline, précision 5),myReport, « À prévoir », photos en objets{ url, credit }(typePhotode Servir « À ne pas manquer » et les photos d'étape avec leur crédit (snapshot du Go, fiche) #508),timezone; aucun avertissement de cohérence ; la 410route_removedavecdetails.removedAtetdetails.near(0,01°), et la colonneroutes.unpublished_atposée parmoveRoute;?trips=estimated;no-storesans ETag et 60 requêtes par minute et par IP ;/timelinesur nos estimations, sans Mapbox, 402premium_requiredsans droit, et ses en-têtes de cache. Critères réécrits en conséquence.needs-design).Le bloqueur #393 est tranché : dépendance retirée,
status:blocked→status:ready, Status Prêts, Sprint 1. Niveau 2 attendu (migration additive, règle d'accès, limite de débit).PaulThiberville commentedon Oct 19, 2026
🔨 Démarrage — plan (CODE, branche
issue/36-route-sheet-api) :0016:routes.unpublished_at, posée parmoveRouteau passage enunpublished, remise ànullà tout autre passage (republication).canAccessRouteétendu à l'auteur (ses propres parcours Premium), parrouteViewerOf, sans changer ses appelants (recherche, programmation, fiche).packages/shared:RouteDetailétendu (geometrydes trajets,myReport, « À prévoir », photos créditées,timezone),RouteRemovedDetails.near, queries strictes de la fiche et de/timeline; fixtures de la fiche régénérées.GET /v1/routes/:id: tableau « Qui obtient quoi », 410route_removedavecremovedAtetnear, étapes verrouillées, trajets exacts de Mapbox pour la réponse seulement (repli estimé),?trips=estimated,no-store, 60 req/min par IP.GET /v1/routes/:id/timeline?startsAt=sur nos estimations, 402 sans droit, en-têtes de cache des trois cas.computeRoutebranché dans l'éditeur pour stockercomputed.Hors de portée tant que #501 n'est pas fusionnée :
resolveBookingLinkn'existe pas encore surmain; la fiche servira le lien du créateur seul et je le signale dans la PR.PaulThiberville commentedon Oct 19, 2026
Point de fermeture et de déploiement, 2026-10-19, MANAGER. Fermé par la PR #536 (
main=659f630, niveau 2, migration additive0016_add_route_unpublished_at).Staging :
deploy-staging.ymlvert sur659f630(run 39252098292, 12:26Z) ; étape « Migrate database » réussie, exécutionwidoo-api-migrate-kkgq6; nouvelle révision saine,/v1/health→{"status":"ok","database":"up"}. Le secretmapbox-directions-tokende staging est vide : la fiche de staging ne sert que des estimations (estimated: true,geometry: null) → #540 (tâche de Paul).Status : reste « À déployer ». L'app ne lit pas encore l'API (fixtures), aucune recette sur appareil n'est possible, et aucune vérification de staging n'a été déclarée par Paul ou Ilan ; Terminés à leur décision.
Critères tenus (cochés, d'après la PR et ses tests) : accès cellule par cellule (
sheet/access.test.ts) ; auteur etcanAccessRouteunique ; Premium sans droit, avec droit et abonnement expiré (route-sheet.test.ts) ; géométrie, rien en base et repli (route-sheet-trips.test.ts) ;?trips=estimatedet clés inconnues ; 410 avecremovedAtetnear,unpublished_atposé et remis ànull;no-storesans ETag et 429 à la 61e requête ;/timeline?startsAt=(route-timeline.test.ts) ; budget exact de 23 €.Partiels (non cochés) :
geometry,myReport,timezoneservis ; photos en objets servies à côté des URL (photos,place.photo) au lieu de les remplacer ; « À prévoir » ne porte que les lignes du créateur (aucune suggestion calculée) ;verification.reportstoujoursnull, aucun lieu signalé.photoUrlsetplace.photoUrl, etstartAccess.linessans le mode.Non tenus (non cochés) : seed v0.2 recalculé par le moteur, et le recalcul de
computedà chaque modification d'étapes ; lien de réservation parresolveBookingLink, qui attend la PR #501 (#344).Suite :
nearettokendanspackages/api-client→ Brancher la fiche parcours de l'app sur GET /routes/:id et /timeline (E-05) #537computedpar le moteur dans l'éditeur, paliers sur/config,mainTransportOfunique, seed recalculé → Recalculer et stocker computed par le moteur dans l'éditeur, et recalculer le seed #538startAccess.linesavec le mode → rattaché à Rafraîchir les stations IDFM et recalculer l'accès hors de l'éditeur #486 (et Calculer l'accès d'un parcours : station la plus proche depuis les données ouvertes d'Île-de-France Mobilités #180)verification.reports→ rattaché à Enregistrer confirmations et signalements de lieux avec pondération et statuts #49resolveBookingLinkdansapps/api/src/sheet/build.ts→ à faire avec ou après la PR feat(admin): affiliation et clics de réservation (A-09) #501