Skip to content

Exposer GET /routes/:id et /routes/:id/timeline, stocker computed #36

Closed

Description

@IlanAMG

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). pending ou changes_requested jamais publié : 200 avec le jeton (sharedBy), 404 sans. pending ou changes_requested déjà publié (un needs_fix corrigé 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é : 400 validation_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 :

  • geometry de chaque trajet (transition.geometry) : polyline encodée, précision 5 (ce que apps/api/src/directions/mapbox.ts demande déjà), null sans 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é, null sinon ; absent ou null sans jeton ;
  • « À prévoir » : lignes du créateur (routes.tips) et suggestions calculées non masquées (computed, moins routes.tips_hidden), D-048, D-050, lues par E-06 ;
  • photos en objets photo { url, credit } (le type Photo de Servir « À ne pas manquer » et les photos d'étape avec leur crédit (snapshot du Go, fiche) #508, credit à null pour une photo de créateur) : photoUrls et place.photoUrl cessent d'être de simples URL ;
  • timezone de la ville (fuseau IANA, comme PlannedRouteSnapshot) : « Ouvert aujourd'hui » se lit à l'heure de Paris.
  • Aucun warnings ni errors de 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 colonne routes.unpublished_at (timestamptz, migration additive), posée par l'écrivain unique du statut (moveRoute) à chaque passage en unpublished, remise à null à la republication.
  • near : coordonnées du départ arrondies à 0,01° (deux décimales), jamais le point exact.
  • ApiError avec apiCode explicite (410 n'a pas de code par défaut dans codeByStatus) ; RouteRemovedDetails (packages/shared) gagne near.

En-têtes et limites :

  • Réponse 200 de GET /routes/:id : Cache-Control: no-store, sans ETag (Q8) ; sauf avec trips=estimated, cachable comme /timeline.
  • 60 requêtes par minute et par IP sur GET /routes/:id, 429 rate_limited au-delà.
  • Erreurs (400, 401, 402, 404, 410, 429) jamais mises en cache.

GET /v1/routes/:id/timeline?startsAt= :

  • startsAt obligatoire (ISO 8601 avec décalage, comme POST /me/planned), token pour 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.
  • RouteTimeline pour ce départ (horaires valables à la date demandée, conflits compris), calculée sur nos estimations, celles que POST /me/planned fige, sans appel à Mapbox (D-093, qui amende D-088).
  • Cache : public (60 s, ETag) sans Authorization ni token ; private, no-cache et Vary: Authorization avec un jeton Firebase ; private, no-store avec un token de 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, pending jamais publié avec jeton, pending déjà publié (404 même avec jeton), draft et rejected, jeton faux (404), identifiant mal formé (400)
  • Test : l'auteur connecté lit son parcours private sans jeton, et son propre parcours Premium en entier sans abonnement ; canAccessRoute reste l'unique règle (recherche, programmation et fiche)
  • Le seed v0.2 est recalculé par le moteur et reste cohérent
  • Test : parcours access = premium sans droit → étape 1 complète, étapes suivantes en LockedStep, aucun appel à Mapbox, geometry: null partout, conflits vides sur les étapes verrouillées ; avec droit → fiche complète ; compte premium dont plan_expires_at est 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_removed avec details.removedAt (lu dans routes.unpublished_at) et details.near arrondi à 0,01°, sans autre donnée du parcours ; unpublished_at posé par moveRoute au 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 dans packages/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 429 rate_limited
  • Test : /timeline?startsAt= sans appel à Mapbox, sur les mêmes estimations que POST /me/planned ; 402 premium_required pour un parcours Premium sans droit ; 400 sans startsAt ; en-têtes de cache des trois cas
  • Test : budgetPerPersonEur exact (23 € reste 23), égal à celui de la card du même parcours, et budgetBucket dé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, bookingIsAffiliate vrai 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 sur GET /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

  1. added this to the v0.3 milestone on Sep 21, 2026
  2. IlanAMG commented on Sep 25, 2026

    @IlanAMG
    MemberAuthor

    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.

  3. IlanAMG commented on Sep 25, 2026

    @IlanAMG
    MemberAuthor

    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.

  4. IlanAMG commented on Sep 25, 2026

    @IlanAMG
    MemberAuthor

    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).

  5. IlanAMG commented on Sep 26, 2026

    @IlanAMG
    MemberAuthor

    D-056 (https://github.com/Inprogress-Agency/widoo-app/wiki/Design-Changelog) : GET /routes/:id expose 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.

  6. IlanAMG commented on Sep 26, 2026

    @IlanAMG
    MemberAuthor

    D-057 (https://github.com/Inprogress-Agency/widoo-app/wiki/Design-Changelog) : GET /routes/:id expose myReport (date du signalement de l'utilisateur connecté s'il date de moins de 7 jours et n'est pas encore tranché, sinon null).

  7. PaulThiberville commented on Sep 29, 2026

    @PaulThiberville
    Collaborator

    📋 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/:id applique 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.

  8. 13 remaining items

  9. PaulThiberville commented on Oct 14, 2026

    @PaulThiberville
    Collaborator

    📋 MANAGER (2026-10-14), découverte de CODE pendant #41 (PR #443, Inbox) : E-06 (« À préparer ») attend les conseils du parcours, que RouteDetail ne 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.tips et 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 (que planned_routes.checklist ne 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 de bookingUrl ni de bookingNote, 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).

  10. PaulThiberville commented on Oct 15, 2026

    @PaulThiberville
    Collaborator

    📋 MANAGER, 2026-10-15 — à prendre en compte ici (découverte de CODE pendant #180, PR #482) : routes.computed.access est maintenant écrit, sous la forme { station, lines: [{ mode, line }], distance_m } (ou null au-delà de 1,5 km). La fiche ne le sert pas encore : RouteDetail.startAccess n'existe que dans les fixtures de l'app (startAccess: null), et son lines: 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/:id devrait servir l'accès avec son mode et sa distance ; l'affichage côté app suivra. La forme de computed.access est à reporter dans Modele-de-Donnees (voir #436).

  11. PaulThiberville commented on Oct 16, 2026

    @PaulThiberville
    Collaborator

    📋 MANAGER, 2026-10-16 — deux ajouts au schéma que ce ticket sert :

    1. « À ne pas manquer » et photos d'étape : Servir « À ne pas manquer » et les photos d'étape avec leur crédit (snapshot du Go, fiche) #508 les ajoute à 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/:id les 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.
    2. Lien de réservation (découverte de CODE sur Admin : affiliation et clics de réservation (A-09) #344, PR #501, Inbox) : chaque étape complète passe par 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.
  12. IlanAMG commented on Oct 16, 2026

    @IlanAMG
    MemberAuthor

    Chef de projet (16/10/2026) : glissé du Sprint 0 au Sprint 1, non terminé.

  13. modified the milestones: v0.3, v0.4 on Oct 16, 2026
  14. IlanAMG commented on Oct 16, 2026

    @IlanAMG
    MemberAuthor

    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).

  15. added
    status:readySpec complète, prêt à coder
    and removed
    status:blockedBloqué, voir commentaire ⛔
    on Oct 19, 2026
  16. PaulThiberville commented on Oct 19, 2026

    @PaulThiberville
    Collaborator

    📋 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.

    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).

  17. PaulThiberville commented on Oct 19, 2026

    @PaulThiberville
    Collaborator

    🔨 Démarrage — plan (CODE, branche issue/36-route-sheet-api) :

    1. Migration additive 0016 : routes.unpublished_at, posée par moveRoute au passage en unpublished, remise à null à tout autre passage (republication).
    2. canAccessRoute étendu à l'auteur (ses propres parcours Premium), par routeViewerOf, sans changer ses appelants (recherche, programmation, fiche).
    3. packages/shared : RouteDetail étendu (geometry des 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.
    4. GET /v1/routes/:id : tableau « Qui obtient quoi », 410 route_removed avec removedAt et near, étapes verrouillées, trajets exacts de Mapbox pour la réponse seulement (repli estimé), ?trips=estimated, no-store, 60 req/min par IP.
    5. GET /v1/routes/:id/timeline?startsAt= sur nos estimations, 402 sans droit, en-têtes de cache des trois cas.
    6. Selon le temps : computeRoute branché dans l'éditeur pour stocker computed.

    Hors de portée tant que #501 n'est pas fusionnée : resolveBookingLink n'existe pas encore sur main ; la fiche servira le lien du créateur seul et je le signale dans la PR.

  18. PaulThiberville commented on Oct 19, 2026

    @PaulThiberville
    Collaborator

    Point de fermeture et de déploiement, 2026-10-19, MANAGER. Fermé par la PR #536 (main = 659f630, niveau 2, migration additive 0016_add_route_unpublished_at).

    Staging : deploy-staging.yml vert sur 659f630 (run 39252098292, 12:26Z) ; étape « Migrate database » réussie, exécution widoo-api-migrate-kkgq6 ; nouvelle révision saine, /v1/health → {"status":"ok","database":"up"}. Le secret mapbox-directions-token de 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 et canAccessRoute unique ; 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=estimated et clés inconnues ; 410 avec removedAt et near, unpublished_at posé et remis à null ; no-store sans ETag et 429 à la 61e requête ; /timeline?startsAt= (route-timeline.test.ts) ; budget exact de 23 €.

    Partiels (non cochés) :

    • Champs de la réponse : geometry, myReport, timezone servis ; 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.reports toujours null, aucun lieu signalé.
    • Schéma partagé : étendu, mais garde photoUrls et place.photoUrl, et startAccess.lines sans 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 par resolveBookingLink, qui attend la PR #501 (#344).

    Suite :

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions