openvibers/openvibe.tours README.md

README.md

main 12.5 KB · 188 lines · markdown

1# OpenVibe.Tours
2
3> Go anywhere: fly, stay, walk, watch.
4
5**Live site:** https://openvibe.tours · **API docs:** https://openvibe.services · **For AI:** https://openvibe.tours/llms.txt
6**Run it:** `npm ci && npm run dev` (Node 22 or newer) · **Tests:** `npm test`
7
8**Status:** v1 written, seeded and tested (see [STATUS.json](STATUS.json)).
9**Domain:** `openvibe.tours` · **Port:** 5170 · **Service id:** `tours` · **Env prefix:** `TOURS`
10**License:** AGPL-3.0-only (the same as every OpenVibe service).
11
12OpenVibe.Tours is the travel room. V1 has six things, all from open data or the network's public APIs:
13
14- **Places.** The top 100 cities from Wikidata and Wikivoyage, then `tourism=*` places from OpenStreetMap through
15 Overpass, with the city's Wikivoyage See listings as a cited fallback when Overpass is queueing. A page per city (`/city/<slug>`) and per place (`/place/<id>`) with the source and the fetch time, plus
16 a **place of the day** chosen deterministically from the same data.
17- **Live tours.** IRL streams from OpenVibe.Live's public stream list, and recent IRL VODs and clips, with a city
18 when the public title or tags name one. Tours reads the public list; it never follows, chats or writes.
19- **Stays.** Active rentals from OpenVibe.Rent's public listings for the city. Tours never takes a payment.
20- **Self-guided routes.** Ordered stops on a map (MapLibre + OpenStreetMap tiles, with a complete SVG and list
21 without JavaScript), generated from Wikivoyage's See lists, marked as generated, every stop cited. TTS audio is
22 off: OpenVibe.AI has no speech-synthesis route today, so every stop says so instead of shipping a dead player.
23- **Flights.** No flight API key in v1. The form opens the official airline-neutral search with the city pair and
24 dates filled in. **No affiliate.** The Duffel/Kiwi seam is off, and a key alone does not turn it on.
25- **Trips.** A signed-in person's trip: a city, dates, saved places, one route, stays, and a read-only share link.
26 Group voting is v2.
27
28## Owns
29
30| Table | Holds | Migration |
31|---|---|---|
32| `tours_cities` | `cty_` + stable id, slug, name, country/ISO code, coordinates, population, Wikidata Q id, Wikivoyage title, summary, image + Commons licence, source URL, fetched time | `migrations/0001_initial.sql` |
33| `tours_city_sources` | the Wikivoyage pages (city + district) a city's summary, See listings and routes come from | `0001_initial.sql` |
34| `tours_places` | `plc_` + stable id, city, OSM type/id, name, `tourism=*` kind, coordinates, address, hours, wheelchair, fee, Wikidata/Wikipedia, image, source URL, fetched time | `0001_initial.sql` |
35| `tours_routes` | `rte_` + stable id, city, slug, title, summary, distance, duration, stop count, generated flag, Wikivoyage source, fetched time | `0001_initial.sql` |
36| `tours_route_stops` | ordered stop, optional place, cited note, coordinates, Wikivoyage URL, audio status/url | `0001_initial.sql` |
37| `tours_live` | `liv_` + stable id, `stream\|vod\|clip`, Live/Media id, username, title, category, viewers, city when named, public URL, times, fetched time | `0001_initial.sql` |
38| `tours_stays` | `sty_` + stable id, Rent `rnt_` id, city, title, kind, price/currency/period, neighbourhood, public URL, fetched time | `0001_initial.sql` |
39| `tours_trips` | `trp_` + ULID, share slug, owner subject, title, city, dates, notes, `private\|shared`, revision | `0001_initial.sql` |
40| `tours_trip_places`, `tours_trip_routes`, `tours_trip_stays` | what the person saved on the trip (cascade with it) | `0001_initial.sql` |
41| `tours_job_runs` | each source job's start/finish, status, counts and error | `0001_initial.sql` |
42| `account_data_events`, `telemetry_samples` | the SDK's account export/deletion receipts and request telemetry (pruned after 14 days) | `0001_initial.sql` |
43
44## Does not own
45
46- **Live, Rent, Media and AI** — Tours reads public Live/Rent pages. Licensed place images are copied to Media with
47 the `media.object.upload` service scope; sourced place summaries use AI with `ai.run.create ai.run.read` when
48 `OV_AI_INTERNAL_URL` is configured. Both keep the original source and attribution on the place page.
49- **Identity and roles** — OpenVibe.Network. Tours is OAuth client `tours` (PKCE S256) and verifies session tokens
50 against the Network JWKS; a person's canonical subject is `usr_…`.
51- **OpenStreetMap, Wikivoyage, Wikidata, Commons** — external open data, cached in Tours' own tables with the source
52 URL and the time fetched; never scraped past their terms.
53- **Events, host and database** — OpenVibe.Events, OpenVibe.Host.
54
55## Sources, licences and fetch times
56
57| Source | Used for | Licence / terms | Kept |
58|---|---|---|---|
59| OpenStreetMap via Overpass | places (`tourism=*`) | ODbL 1.0; contact User-Agent, per-host pacing, one bounded `around:` query, no bulk download | OSM object URL, fetched time |
60| Wikivoyage | city summary, See listings, route stops | CC BY-SA 4.0; the page URL on every city and stop | page URL, fetched time |
61| Wikidata | coordinates, population, country, English Wikivoyage sitelink | CC0 | Q id, fetched time |
62| Wikimedia Commons and Openverse | place and city images with licence/credit | CC BY, CC BY-SA, CC0 or public domain; no NC/ND | source URL, credit, licence, Media thumbnail |
63| OpenVibe.Live | live IRL streams, VODs, clips | public API; read-only | item URL, fetched time |
64| OpenVibe.Rent | stays | public listings; read-only | listing URL, fetched time |
65
66OpenVibe.AI may summarize the cited place description for “Why go”; the page labels this as an AI summary and links
67the source. If the AI call is unavailable, the sourced description remains. AI has no speech synthesis route; route
68audio remains off.
69
70## Jobs
71
72Each source has its own job in `server/jobs/`, and each writes a receipt in `tours_job_runs` (status, counts, error).
73They are off under `TOURS_DRILL`; production starts them after listen and refreshes on their own interval.
74
75| Job | Fills | Default interval | Env |
76|---|---|---|---|
77| `cities` | 100 cities, their Wikidata/Commons image and Wikivoyage page + See lists | 24 h | `TOURS_CITIES_INTERVAL_MS`, `TOURS_CITY_LIMIT`, `TOURS_WIKIVOYAGE_DISTRICTS` |
78| `places` | Wikivoyage See places for every city, then Overpass `tourism=*` for the 10 cities whose OSM data is oldest | 7 d | `TOURS_PLACES_INTERVAL_MS`, `TOURS_OSM_RADIUS_M`, `TOURS_OSM_MAX_PER_CITY`, `TOURS_OSM_CITIES_PER_RUN` |
79| `routes` | up to two cited self-guided routes per city from the See lists | 7 d | `TOURS_ROUTES_INTERVAL_MS` |
80| `live` | IRL streams now, recent IRL VODs/clips | 2 min | `TOURS_LIVE_INTERVAL_MS` |
81| `stays` | Rent listings for the city | 6 h | `TOURS_STAYS_INTERVAL_MS` |
82
83`npm run seed` runs them outside the server; the first run's counts are in the report and in `tours_job_runs`.
84The first local seed (2026-10-31) stored **100 cities**, **2,714 places** (2,406 Wikivoyage + 308 OpenStreetMap across
8510 cities), **180 routes / 1,239 cited stops** (96 cities), **23 IRL VODs + 1 clip**, and **0 stays** (Rent had no
86active listing; the shelf says so). Live had no stream at that moment.
87A failed source leaves the previous rows alone and is reported honestly; a city with fewer than four cited stops
88with coordinates gets no invented route.
89
90## API
91
92Everything is JSON under `/api/v1`; errors are `application/problem+json` with a stable code. A person is the
93`tours_at` session cookie or `Authorization: Bearer <Network user token>`, verified offline with the Network JWKS.
94A write made with the session cookie must come from `https://openvibe.tours` (`403 tours.cross_site`). No route
95needs a service token.
96
97| Route | What it does |
98|---|---|
99| `GET /api/v1/cities?limit=&offset=&q=` | the top cities, population first, with place/route counts |
100| `GET /api/v1/cities/:slug` | one city with its places, routes, live tours and stays |
101| `GET /api/v1/places/:id` | one place and the routes it is on |
102| `GET /api/v1/routes/:id` | one route with its ordered, cited stops |
103| `GET /api/v1/live` | IRL tours now, recent IRL VODs and clips |
104| `GET /api/v1/openapi.json` | the OpenAPI 3.1 document |
105| `GET /api/v1/trips` | the signed-in person's trips |
106| `POST /api/v1/trips` | create `{ title, city_slug, starts_on, ends_on, notes?, visibility? }` |
107| `GET /api/v1/trips/:slug` | own trip, or one shared with you |
108| `PATCH /api/v1/trips/:slug` | edit title, city, dates, notes or visibility |
109| `DELETE /api/v1/trips/:slug` | delete the trip (204) |
110| `POST /api/v1/trips/:slug/places` | save a place `{ place_id, note? }` |
111| `DELETE /api/v1/trips/:slug/places/:placeId` | remove it |
112| `POST /api/v1/trips/:slug/route` | set/clear the route `{ route_id }` (must be in the trip's city) |
113| `POST /api/v1/trips/:slug/stays` | add a stay `{ stay_id }` |
114| `DELETE /api/v1/trips/:slug/stays/:stayId` | remove it |
115
116Validation refusals are `400 tours.invalid` or `400 tours.trip.invalid`; other codes: `tours.sign_in_required`,
117`tours.person_required`, `tours.cross_site`, `tours.city_not_found`, `tours.place_not_found`,
118`tours.route_not_found`, `tours.trip_not_found`, `tours.trip_owner`, `tours.rate_limited`.
119
120## Pages
121
122Server-rendered through the OpenVibe Frame (`openvibe-shared/shell`) and the room kit, readable without JavaScript.
123Public pages are `public, max-age=60` with `Vary: Cookie`; a page drawn for a signed-in person or a private trip is
124`private, no-store`; a shared trip is public and read-only. Ids and classes start with `tr-`.
125
126| Page | What it is |
127|---|---|
128| `/` | the room: hero, live tours now, place of the day, top cities, routes, stays, scenes, FAQ, developers |
129| `/cities` | the catalogue with a search box |
130| `/city/:slug` | summary, image licence, places, routes, live, stays and a trip/ flight start |
131| `/place/:id` | a place, its facts, OSM link, source, routes it is on, and save-to-trip |
132| `/routes`, `/routes/:id` | the route list; one route with its map, stops, citations and audio note |
133| `/live`, `/stays` | IRL tours now/VODs/clips; Rent stays with add-to-trip |
134| `/flights` | the form that links the official airline-neutral search; the provider note |
135| `/trips`, `/trips/new`, `/trips/:slug` | a person's trips; create; view/edit/share/delete |
136| `/developers` | the API table, OpenAPI link, sources and job receipts |
137| `/robots.txt`, `/sitemap.xml`, `/llms.txt`, `/llms-full.txt` | discovery; trips and the API are excluded |
138
139The route map is MapLibre + OSM raster tiles from a CDN, loaded only on a route page; the same page always has the
140numbered SVG map and every stop in text. `public/css/tours.css` is a working layout, not the final design: the
141orchestrator designs every page.
142
143## Sign-in, trips and privacy
144
145A trip belongs to one Network `usr_…` subject. Only the owner can change it; a shared trip is readable by anyone
146with the link and editable only by the owner; a private trip is a 404 to anyone else. The share slug is random, not
147sequential. Account export returns the person's trips; account deletion erases the trips and everything saved on
148them (`server/identity/account-data.js`). No table holds a token, and `/auth/me` never returns one.
149
150## Run and test
151
152```sh
153npm install --include=dev
154npm test # PGlite, fixtures, no network; 10 files
155npm run test:pg # the same through PostgreSQL + PgBouncer
156npm run seed # the real sources into data/pglite (development)
157TOURS_CITY_LIMIT=10 npm run seed
158npm start # PORT 5170, loopback; DATABASE_URL or embedded PGlite in development
159```
160
161The tests cover the migration and its constraints; every source parser and job against fixture sources (including
162the Commons licence, OSM User-Agent, stable ids and idempotent reruns); the flights URL and no-affiliate rule; the
163accent's AA contrast in both themes; every API route and its refusals; the full trip CRUD and sharing rules; pages
164and their TouristTrip/TouristAttraction JSON-LD; sign-in with PKCE and session rules; and discovery.
165
166## Deploy
167
168`deploy/systemd/openvibe-tours.service` and `deploy/nginx/openvibe.tours.conf` are Meme's files with the names and
169port changed (5170, `/etc/openvibe/tours.env`, `/opt/openvibe.tours`); the forms zone covers `/trips`. The
170`.gitignore` is Meme's exact file. Not deployed by this change: the orchestrator creates the repo, the Contracts
171manifest, the Network client and the host.
172
173## Accent
174
175Room id `tours` in `openvibe-shared/rooms`: dark `#F98942` on `#0B0D12` is **8.04:1** and light `#AA3900` on white is
176**6.37:1** — AA in both themes (tested in `test/accent.test.js`).
177
178## License
179
180AGPL-3.0-only. See [LICENSE](LICENSE).
181
182<!-- versions:start -->
183- openvibe-contracts: v0.419.0
184- openvibe-sdk: v0.161.0
185- openvibe-shared: v4.241.0
186- openvibe-publishing: v1.7.0
187<!-- versions:end -->
188