- 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
136 lines
6.8 KiB
Markdown
136 lines
6.8 KiB
Markdown
# 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/me`
|
|
- `GET/POST /api/trips` · `GET/PATCH/DELETE /api/trips/:id`
|
|
- `POST /api/trips/join` (by join code) · `POST /api/trips/:id/join-code` (regenerate)
|
|
- `GET/POST /api/trips/:id/entries` · `PATCH/DELETE /api/entries/:id`
|
|
- `GET /api/trips/:id/route` (legs + km + summary, computed server-side)
|
|
- `GET /api/geocode?q=...` (Nominatim proxy)
|
|
|
|
## Running It
|
|
|
|
```bash
|
|
# 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
|
|
|
|
1. **Scaffold** — repo, package.json, this doc, API contract. ✅
|
|
2. **Swarm build** (parallel agents): backend API + DB · frontend SPA (calendar, map, summary) · Docker/infra + tests.
|
|
3. **Integrate & verify** — run tests, boot server, smoke-test in browser, build Docker image.
|
|
4. Initial commit.
|