No description
Find a file
Claudio Schaad 9b35f6222f Add a city list view
New nav item between Map and Users shows every tracked city as a
table (photo, name, country, status, visit dates, co-travellers),
ordered with planned trips first and newest visits on top within
each group.
2026-07-21 20:23:14 +02:00
client Add a city list view 2026-07-21 20:23:14 +02:00
server Add CityTracker v1: map-based city tracker with multi-user support 2026-07-20 20:29:25 +02:00
shared Add CityTracker v1: map-based city tracker with multi-user support 2026-07-20 20:29:25 +02:00
.dockerignore Add CityTracker v1: map-based city tracker with multi-user support 2026-07-20 20:29:25 +02:00
.env.example Add CityTracker v1: map-based city tracker with multi-user support 2026-07-20 20:29:25 +02:00
.eslintrc.json Add CityTracker v1: map-based city tracker with multi-user support 2026-07-20 20:29:25 +02:00
.gitignore Add CityTracker v1: map-based city tracker with multi-user support 2026-07-20 20:29:25 +02:00
.prettierrc.json Add CityTracker v1: map-based city tracker with multi-user support 2026-07-20 20:29:25 +02:00
docker-compose.yml Add CityTracker v1: map-based city tracker with multi-user support 2026-07-20 20:29:25 +02:00
Dockerfile Add CityTracker v1: map-based city tracker with multi-user support 2026-07-20 20:29:25 +02:00
LICENSE Initial commit 2026-07-20 15:06:56 +00:00
package-lock.json Add CityTracker v1: map-based city tracker with multi-user support 2026-07-20 20:29:25 +02:00
package.json Add CityTracker v1: map-based city tracker with multi-user support 2026-07-20 20:29:25 +02:00
README.md Add CityTracker v1: map-based city tracker with multi-user support 2026-07-20 20:29:25 +02:00

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)

This is the intended way to run CityTracker — one image, one volume.

  1. Copy the example environment file and fill in real values:

    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:

    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).

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):

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.