# 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://:` (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:` (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/` 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.