openvibers/openvibe.tours README.md
README.md
| 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 | |
| 12 | OpenVibe.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 | |
| 66 | OpenVibe.AI may summarize the cited place description for “Why go”; the page labels this as an AI summary and links |
| 67 | the source. If the AI call is unavailable, the sourced description remains. AI has no speech synthesis route; route |
| 68 | audio remains off. |
| 69 | |
| 70 | ## Jobs |
| 71 | |
| 72 | Each source has its own job in `server/jobs/`, and each writes a receipt in `tours_job_runs` (status, counts, error). |
| 73 | They 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`. |
| 84 | The first local seed (2026-10-31) stored **100 cities**, **2,714 places** (2,406 Wikivoyage + 308 OpenStreetMap across |
| 85 | 10 cities), **180 routes / 1,239 cited stops** (96 cities), **23 IRL VODs + 1 clip**, and **0 stays** (Rent had no |
| 86 | active listing; the shelf says so). Live had no stream at that moment. |
| 87 | A failed source leaves the previous rows alone and is reported honestly; a city with fewer than four cited stops |
| 88 | with coordinates gets no invented route. |
| 89 | |
| 90 | ## API |
| 91 | |
| 92 | Everything 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. |
| 94 | A write made with the session cookie must come from `https://openvibe.tours` (`403 tours.cross_site`). No route |
| 95 | needs 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 | |
| 116 | Validation 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 | |
| 122 | Server-rendered through the OpenVibe Frame (`openvibe-shared/shell`) and the room kit, readable without JavaScript. |
| 123 | Public 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 | |
| 139 | The route map is MapLibre + OSM raster tiles from a CDN, loaded only on a route page; the same page always has the |
| 140 | numbered SVG map and every stop in text. `public/css/tours.css` is a working layout, not the final design: the |
| 141 | orchestrator designs every page. |
| 142 | |
| 143 | ## Sign-in, trips and privacy |
| 144 | |
| 145 | A trip belongs to one Network `usr_…` subject. Only the owner can change it; a shared trip is readable by anyone |
| 146 | with the link and editable only by the owner; a private trip is a 404 to anyone else. The share slug is random, not |
| 147 | sequential. Account export returns the person's trips; account deletion erases the trips and everything saved on |
| 148 | them (`server/identity/account-data.js`). No table holds a token, and `/auth/me` never returns one. |
| 149 | |
| 150 | ## Run and test |
| 151 | |
| 152 | ```sh |
| 153 | npm install --include=dev |
| 154 | npm test # PGlite, fixtures, no network; 10 files |
| 155 | npm run test:pg # the same through PostgreSQL + PgBouncer |
| 156 | npm run seed # the real sources into data/pglite (development) |
| 157 | TOURS_CITY_LIMIT=10 npm run seed |
| 158 | npm start # PORT 5170, loopback; DATABASE_URL or embedded PGlite in development |
| 159 | ``` |
| 160 | |
| 161 | The tests cover the migration and its constraints; every source parser and job against fixture sources (including |
| 162 | the Commons licence, OSM User-Agent, stable ids and idempotent reruns); the flights URL and no-affiliate rule; the |
| 163 | accent's AA contrast in both themes; every API route and its refusals; the full trip CRUD and sharing rules; pages |
| 164 | and 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 |
| 169 | port 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 |
| 171 | manifest, the Network client and the host. |
| 172 | |
| 173 | ## Accent |
| 174 | |
| 175 | Room 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 | |
| 180 | AGPL-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 |