Stories
A finished ride is a track, a set of photos and a route you chose for a reason. The story tools turn that into something worth showing someone.
AI trip stories
POST /enrich/trip-story starts a job that reads a recorded ride and writes it
up. Generation is asynchronous — the endpoint returns a job, and you poll it:
| Start | POST /enrich/trip-story |
| List your jobs | GET /enrich/jobs |
| One job's state | GET /enrich/jobs/{job_id} |
| The finished story | GET /enrich/jobs/{job_id}/story |
| Delete | DELETE /enrich/jobs/{job_id} |
ENRICH_STORY_LANGS sets which languages a story is generated in, and
ENRICH_MAX_ACTIVE_JOBS caps how many run at once.
Along the way the enrichment gateway can pull context about what you rode past:
GET /enrich/wiki/nearby finds Wikipedia articles near a point and
GET /enrich/wiki/summary/{page_id} fetches one.
Which AI writes it
A story is written by a language model, and the server chooses which one. Two of them are the owner's own GPUs at home; the third is the Claude API, used when the home machines are asleep or busy — and for the translations, where volume makes a cheaper model the right one. What is sent is the material a story is made of: place names, distances, times and the captions the home GPU wrote for your photos. Your photos and your raw track never leave the server, and nothing carries your name unless you typed one. The Claude API is a processor outside the EU with a 30-day retention, which is why it is only ever used for a feature you asked for by starting a job.
Editing and publishing
A generated story is a draft, not a verdict. PUT /enrich/jobs/{job_id}/edits
stores your changes as an overlay on the generated text, and
POST /enrich/jobs/{job_id}/republish rebuilds the published page with them
applied. Editing never destroys the original generation.
Publishing gives the story a capability slug — a public URL under
/enrich/view/{slug}/… that anyone can open without an account. That is
deliberate: a story is meant to be sent to someone.
Two consequences worth knowing:
- The published page is served from a private store by the enrichment router, not a public static mount, so the slug is genuinely the only way in.
POST /enrich/jobs/{job_id}/revokewithdraws the slug. Anyone holding the old link loses access immediately.
StoryMaps
A StoryMap is the same material as a scrolling 3D map: the reader moves through the ride while the globe flies along with them. StoryMaps are authored in the desktop client and published the same way, through the enrichment jobs above.
Rendering the globe needs a Cesium ion token, which the deployment sets as
CESIUM_ION_TOKEN; without it the map surface will not load.
Looking around
A StoryMap drives the camera for you: you scroll, it travels, and dragging turns your head without leaving the spot. When you want to go somewhere it isn't taking you, hold ⌥ Option (or ⌘ Command) and the globe becomes an ordinary map — drag to pan, scroll or pinch to zoom, right-drag to tilt. Let go and the story carries on from wherever you left the camera.
Press M to keep the map on without holding a key, and Esc to leave it.
On a recap, the speed button turns on a readout: hover a rider and you get their name and how fast they went. It is their average over the whole ride — a recap keeps each line and its total time, not a reading per moment — and the button only appears when the rides carry times at all.
The recap of an event
An event's recap is one StoryMap with several riders on it: everybody's line drawn together, each in its own colour and labelled with the rider's name. A trip story is about your own ride; a recap is about the day you all had.
| Build it | POST /events/{event_id}/recap |
| Find it | GET /events/{event_id}/recap |
The event's owner or an organiser builds it, once the event is finished. What
goes in is exactly what the event already shares — the rides its riders offered
back as result tracks, and the media they attached. Nothing is read from
anybody's library, and a ride a rider kept to themselves is not in it. Rides
recorded outside the event's own window are left out, and the answer tells you
which and why.
The recap is shared with the event's roster and with nobody else: one share,
viewer only, no public link. Every roster member can read GET /events/{event_id}/recap to open it; somebody who was not on the roster gets
the same "no such event" as a stranger.
On the desktop it lives on the event's console: the organiser makes it from the Overview, which says what goes in and who will see it first, and every rider on the roster opens it from the same place. The job shows on the Stories page too, as an Event recap. It has no link to copy there, because it belongs to the roster, and dismissing it takes it away from the whole roster, which the page says before it does it.
:::caution Being written The authoring walkthrough — choosing chapters, camera moves and cover images — is still to be written up. The endpoints above are complete and documented in the API reference under the enrichment tag. :::