CityTracker/README.md
Claudio Schaad 3debb1ce1e Add CityTracker v1: map-based city tracker with multi-user support
React/Vite frontend and Express/SQLite backend for tracking visited and
planned cities on a map, with per-city notes, photos, links, and
participant-based sharing between family members. Includes session auth
with admin-managed user accounts, an OSM/Nominatim geocoding proxy, and a
single-image Docker deployment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-20 20:29:25 +02:00

154 lines
6.2 KiB
Markdown

# CityTracker
A self-hostable webapp for tracking cities you've visited (or plan to visit) on a
map, with ratings, notes, photos, and links — built for sharing between family
members or a partner, each with their own login.
## Features
- Interactive world map (Leaflet + OpenStreetMap tiles, no API keys) with pins
colored by status (visited / planned) and a heart badge for favorites
- Add a city by searching its name (via OpenStreetMap Nominatim) or by clicking
directly on the map
- Per-city notes, a "liked" heart, one photo, a list of links, and the people
who were there (picked from your user list)
- Filter the map by visited/planned, favorites, year, or country
- Multiple user accounts; a city is visible/editable by everyone listed as a
participant on it
- Admin-managed user accounts (no public self-registration)
- Single Docker image, SQLite database, file-based photo storage — easy to back
up as one Docker volume
## Tech stack
- **Frontend:** React + TypeScript, Vite, React Router, TanStack Query, Leaflet
- **Backend:** Node.js + TypeScript, Express, better-sqlite3, argon2, multer
- **Database:** SQLite, single file, plain SQL migrations run automatically on
startup
- **Auth:** server-side sessions (httpOnly cookies), backed by a SQLite table so
logins survive container restarts
## Project structure
```
CityTracker/
├── shared/ # TypeScript types + zod validation schemas, used by both client and server
├── server/ # Express API (auth, cities, uploads, geocoding proxy)
└── client/ # React app (Vite)
```
## Running with Docker (recommended)
This is the intended way to run CityTracker — one image, one volume.
1. Copy the example environment file and fill in real values:
```sh
cp .env.example .env
```
At minimum, set:
- `SESSION_SECRET` — a long random string (generate one with
`node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"`)
- `ADMIN_USERNAME` / `ADMIN_PASSWORD` — credentials for the first admin
account, created automatically the first time the server starts. If you
leave these unset, open the app once it's running and use the one-time
`/setup` screen instead.
2. Build and start the container:
```sh
docker compose up -d --build
```
3. Open `http://<your-host>:<HOST_PORT>` (default port `3000`) and log in with
the admin account you configured.
All application data — the SQLite database and uploaded photos — lives under
`/app/data` inside the container, on the `citytracker-data` named volume. Back
up that volume (e.g. `docker run --rm -v citytracker_citytracker-data:/data -v $(pwd):/backup alpine tar czf /backup/citytracker-backup.tar.gz /data`)
to back up everything.
### Behind Nginx Proxy Manager (or another reverse proxy)
The container just needs to be reachable and listening on `HOST_PORT`. Point a
proxy host at `docker.covenant.lan:<HOST_PORT>` (or wherever the container
runs), enable SSL as usual in your proxy. If your proxy terminates TLS and
forwards plain HTTP to the container, set `COOKIE_SECURE=true` in `.env` so
session cookies are still marked `Secure` correctly; leave it `false` if the
container itself is only ever reached over plain HTTP inside your LAN.
### First admin account
On first startup, if no admin user exists yet:
- If `ADMIN_USERNAME` and `ADMIN_PASSWORD` are set in `.env`, that account is
created automatically.
- Otherwise, visiting the app shows a one-time setup screen to create the
first admin account by hand.
After that, only an admin can create further user accounts, from the **Users**
page in the app.
## Adding more users
Log in as an admin, go to **Users**, and use the "Add a user" form. Admins can
also promote/demote other admins, deactivate/reactivate accounts, and reset
passwords from the same page. Deactivated accounts can no longer log in but
remain visible (grayed out) on cities they previously participated in, so
history isn't lost.
## Local development (without Docker)
Requires Node.js 20 (native modules `better-sqlite3` and `argon2` need a
recent Node build with prebuilt binaries available).
```sh
npm install
npm run build -w shared # shared types must be built once before starting the server or client
# in one terminal
PORT=3000 SESSION_SECRET=dev-secret ADMIN_USERNAME=admin ADMIN_PASSWORD=devpassword123 npm run dev:server
# in another terminal
npm run dev:client
```
The Vite dev server (`http://localhost:5173`) proxies `/api` and `/uploads`
requests to the backend on port 3000.
To produce a production build of everything (used by the Docker image):
```sh
npm run build
npm start
```
## Configuration reference
All configuration is via environment variables (see `.env.example`):
| Variable | Description | Default |
|---|---|---|
| `PORT` | Port the server listens on inside the container | `3000` |
| `HOST_PORT` | Port exposed on the Docker host (docker-compose only) | `3000` |
| `SESSION_SECRET` | Secret used to sign session cookies. **Required** in production. | — |
| `ADMIN_USERNAME` / `ADMIN_PASSWORD` | Credentials for the auto-created first admin account | — |
| `DB_PATH` | Path to the SQLite database file | `/app/data/citytracker.db` (Docker) |
| `UPLOAD_DIR` | Directory where uploaded photos are stored | `/app/data/uploads` (Docker) |
| `COOKIE_SECURE` | Mark session cookies `Secure` (only if TLS reaches the container or is terminated just in front of it) | `false` |
## Notes on scope / what's not included
- **Trips** (grouping cities into a named trip with its own date range) is
planned as a future addition. The database schema was deliberately kept
simple enough that adding `trips` / `trip_cities` tables later won't require
reworking existing data.
- The UI is English-only by design, to keep the app simple; phone/browser
auto-translate tools work fine over it if needed.
- Uploaded photos are served from `/uploads/<random-filename>` without a
per-request auth check — acceptable for a private LAN/home-use deployment
since filenames are non-guessable UUIDs, but worth knowing if you expose
this instance beyond your own network.
- There is no automated test suite; this is a personal/family-scale project
verified through manual end-to-end testing.