- Type set is now activity/stay/transport/flight/rental/note; hotel, travel and immigration are removed with idempotent startup data migrations (hotel->stay, travel->transport, immigration->activity with flag prefix) - Transport entries carry an optional mode (train/bus/ferry/taxi/drive/other) that drives the chip/map icon; route stops expose transport_mode - Creating a stay auto-creates a bridging transport to its neighbouring stays unless a transport/flight already covers the gap (one-shot) - Summary: transports count replaces hotels/travelLegs
6.8 KiB
Trip Plan — Project Overview
A self-hosted, multi-user web tool for collaboratively planning trips. Runs as a single Docker container.
Core Idea
Multiple people log in to the same instance. Anyone can create a trip by picking a name and a date range. The tool generates a day-by-day calendar for that range, and every member of the trip can fill in what happens on each day — flights, stays, transports, activities. The trip is visualized on an interactive map with the route drawn between stops, distances per leg, and an overall summary (days, nights, flights, total km).
Features
1. Users & Access (privacy-friendly, Mullvad-style)
- No registration form, no email, no password. Click "Create account" and you get a random account number (e.g.
7K2M-9QX4-TT8B-3WPF) — that token IS your login. It's shown once; only its hash is stored server-side. - Every account gets an auto-generated, editable display name (e.g.
brave-otter) so members and cost splits stay human-readable. - Trips are shared via a join code: every trip has a short code (e.g.
M4TH-8RK2); anyone with an account can enter it under "Join a trip" to become an editor. The owner can regenerate the code to cut off further joins.
2. Trips
- Create a trip with name + start date + end date.
- Editing the date range regenerates the calendar (existing day entries outside the new range are kept but flagged).
- Trip list dashboard showing all trips you're a member of.
3. Day-by-Day Calendar
- Calendar grid generated from the trip's date range (weeks as rows, like a real calendar).
- Click any day to open the day editor and add entries:
| Entry type | Typical fields |
|---|---|
| 📍 Activity / sightseeing | place, time, notes |
| 🏙️ Stay (area block / accommodation) | location, from → until dates, hotel name in title, price |
| 🚆 Transport | mode (train/bus/ferry/taxi/drive), from → to; auto-created between consecutive stays |
| ✈️ Flight | flight no., from/to airports, departure/arrival time, multi-leg segments |
| 🚙 Rental car | pickup/dropoff, included km |
| 📝 Note | free text |
(Legacy hotel/travel/immigration entries are migrated automatically at startup: hotel → stay, travel → transport, immigration → activity with a 🛂 title prefix.)
- Every entry can have a location (searched via OpenStreetMap geocoding — type "Chiang Mai" and pick from suggestions; lat/lng stored automatically).
- Entries show as compact chips inside the day cell; multiple entries per day, ordered.
4. Map Visualization
- Interactive map (Leaflet + OpenStreetMap tiles — free, no API key required, unlike Google Maps).
- All located entries plotted as markers, numbered in chronological order.
- Route polyline connecting the stops in order.
- Distance per leg (km, great-circle) shown on the route and in a leg-by-leg list.
5. Costs & Splitting
- Every entry can carry a price (flights, stays, car rental, train tickets, activities…), in the trip's currency (one currency per trip, no FX conversion).
- Each priced entry records who paid and how it's split:
equal— total split equally among selected participants (e.g. rental car 50/50)own— price is per person, everyone pays their own (e.g. flights)payer— personal expense of the payer
- Cost summary per trip: total cost, breakdown by type, per-user share vs. paid vs. net balance, and settle-up suggestions (who transfers how much to whom).
6. Summary
- Total days and nights.
- Number of flights, stays, transports.
- Total distance in km across the whole trip.
- Countries/locations visited (from entry locations).
Tech Stack
| Layer | Choice | Why |
|---|---|---|
| Backend | Node.js 20 + Express | Small, ubiquitous, one runtime in the container |
| Database | SQLite (better-sqlite3) | Zero-config single file; persisted via Docker volume |
| Auth | cookie sessions + bcrypt | Simple and adequate for a self-hosted tool |
| Frontend | Vanilla JS SPA (no build step) | Keeps image small and the stack simple |
| Map | Leaflet + OpenStreetMap | Free, no API key, offline-friendly tiles options |
| Geocoding | Nominatim (proxied server-side) | Free location search, no key |
| Packaging | Dockerfile + docker-compose | docker compose up and you're running |
Architecture
trip_plan/
├── src/server/ # Express app
│ ├── index.js # bootstrap, static serving
│ ├── db.js # SQLite schema + connection
│ ├── auth.js # session middleware
│ ├── routes/ # auth, trips, entries, geocode
│ └── util/distance.js # haversine km calc
├── public/ # SPA served statically
│ ├── index.html
│ ├── css/styles.css
│ └── js/ # api client, views (trips, calendar, map, summary)
├── tests/ # API tests (node:test + supertest)
├── data/ # SQLite file (gitignored, volume-mounted)
├── docs/ # this file + API.md
├── Dockerfile
└── docker-compose.yml
Data Model
users (id, username, password_hash, created_at)
trips (id, name, start_date, end_date, owner_id, created_at)
trip_members (trip_id, user_id, role) -- owner | editor
entries (id, trip_id, date, type, title, details,
start_time, end_time, location_name, lat, lng, sort_order)
Route + km are derived: located entries ordered by (date, sort_order) form the route; each consecutive pair is a leg with haversine distance.
API (REST, JSON, under /api)
POST /api/auth/account(create token account) ·POST /api/auth/login·POST /api/auth/logout·GET/PATCH /api/auth/meGET/POST /api/trips·GET/PATCH/DELETE /api/trips/:idPOST /api/trips/join(by join code) ·POST /api/trips/:id/join-code(regenerate)GET/POST /api/trips/:id/entries·PATCH/DELETE /api/entries/:idGET /api/trips/:id/route(legs + km + summary, computed server-side)GET /api/geocode?q=...(Nominatim proxy)
Running It
# development
npm install
npm start # http://localhost:3000
# production
docker compose up -d # builds image, mounts ./data for the SQLite file
Non-Goals (for now)
- Real-time collaborative editing (last-write-wins is fine for this scale)
- Driving-route distances via a routing engine (great-circle km first; OSRM could be added later)
- Email invites / password reset (admin resets via CLI)
Build Plan
- Scaffold — repo, package.json, this doc, API contract. ✅
- Swarm build (parallel agents): backend API + DB · frontend SPA (calendar, map, summary) · Docker/infra + tests.
- Integrate & verify — run tests, boot server, smoke-test in browser, build Docker image.
- Initial commit.