# Travel Monitor A self-hosted real-time travel time dashboard. Shows how long it takes to drive from a fixed HQ location to multiple offsite destinations, with color-coded status based on current traffic conditions relative to a learned baseline. Built for homelabs. Runs as a single Docker container. --- ## Features - **Real-time routing** via pluggable provider (Google, Mapbox, HERE, OpenRouteService) - **Baseline-relative status** — green/yellow/red/dark-red based on how much worse current traffic is vs. normal - **Interactive map** with numbered pins, route display on click, and multiple map styles (Dark Matter, Voyager, Positron, OSM, Satellite) - **Glanceable sidebar** with color-coded rows, travel time both ways, and delay percentage - **Stats tab** — sortable table by time, distance, or delay - **Location management** — add by address or coordinates, rename inline, drag to reorder, delete with confirmation - **Favicon + tab title** update to reflect worst current status - **Debug mode** — type `thisisdebug` anywhere on the page to unlock traffic simulation tools - **Keyboard shortcuts** — `R` refresh, `1–9` jump to location, `Tab` switch tabs, `?` show shortcuts, `Esc` close - **Resizable sidebar** — drag the edge, persists across reloads - **Provider status indicator** — shows in header when any location fails to fetch, with error details - **Rate limiting** on all API endpoints via slowapi - **Mobile layout** — stacked map/list, bottom navigation, touch drag-to-reorder, collapsible map - **Dark mode only** — as it should be --- ## Requirements - Docker + Docker Compose - API key for your chosen routing provider (see Routing Providers below) --- ## Setup ```bash # 1. Install Docker on your LXC (if not already) curl -fsSL https://get.docker.com | sh # 2. Copy files to the LXC scp -r travel-monitor/ root@:/opt/travel-monitor # 3. SSH in and configure ssh root@ cd /opt/travel-monitor cp .env.example .env nano .env # 4. Build and start docker compose up -d # 5. Check logs docker compose logs -f ``` Open `http://:` in your browser. --- ## Configuration (`.env`) | Variable | Default | Description | |---|---|---| | `PORT` | `8000` | Host port to expose | | `APP_TITLE` | `Travel Monitor` | Title shown in header and browser tab | | `HQ_NAME` | — | Friendly name for HQ shown in header (optional) | | `HQ_ADDRESS` | — | HQ street address (geocoded on startup) | | `HQ_LAT` / `HQ_LNG` | — | HQ coordinates (alternative to address) | | `ROUTING_PROVIDER` | `openrouteservice` | See routing providers below | | `ORS_API_KEY` | — | OpenRouteService key | | `GOOGLE_API_KEY` | — | Google Maps key | | `MAPBOX_API_KEY` | — | Mapbox key | | `HERE_API_KEY` | — | HERE key | | `REFRESH_INTERVAL` | `300` | Seconds between auto-refreshes | | `THRESHOLD_YELLOW` | `1.20` | Ratio where green → yellow (20% worse than baseline) | | `THRESHOLD_RED` | `1.50` | Ratio where yellow → red (50% worse) | | `THRESHOLD_DARK_RED` | `1.70` | Ratio where red → dark red (70% worse) | | `DATA_DIR` | `/data` | Path inside container for locations.json | --- ## Routing Providers Set `ROUTING_PROVIDER` to one of the following and supply the matching API key. | Provider | Value | Free tier | Traffic-aware | Notes | |---|---|---|---|---| | OpenRouteService | `openrouteservice` | 2,000 req/day | ❌ No | Returns static historical times. Fine for testing, not for real traffic monitoring. | | Google Maps | `google` | ~$200/month credit | ✅ Yes | Best routing quality. Uses `departure_time=now` + `duration_in_traffic`. | | Mapbox | `mapbox` | 100,000 req/month | ✅ Yes | Uses `driving-traffic` profile. Generous free tier. | | HERE | `here` | 250,000 req/month | ✅ Yes | Uses `departureTime=now`. Good European coverage. | **Recommendation:** Mapbox or Google for real traffic data. OpenRouteService only if you don't need live conditions. --- ## Status colors | Color | Ratio | Meaning | |---|---|---| | 🟢 Green | < 1.20× | Normal — within 20% of baseline | | 🟡 Yellow | 1.20–1.50× | Worsened — 20–50% slower than baseline | | 🔴 Red | 1.50–1.70× | Bad — 50–70% slower | | ⬛ Dark red | > 1.70× | Reconsider — more than 70% slower | Baselines are learned from the first successful fetch per location. To reset a baseline, use the `⋯` menu on any location → Reset baseline. --- ## Data persistence Locations and baselines are stored in `./data/locations.json` on the host, mounted into the container. Survives container restarts and rebuilds. To manually reset a baseline without the UI, edit `./data/locations.json` and set `"baseline_seconds": null` for that entry. --- ## Rate limits All API endpoints are rate-limited per IP via slowapi: | Endpoint category | Limit | |---|---| | `GET /api/config`, `GET /api/locations` | 60/min | | `GET /api/status` | 30/min | | `POST /api/refresh` | 10/min | | Write operations (add, rename, delete, reorder, reset) | 20/min | | Debug (simulate-delay) | 20/min | --- ## Docker commands ```bash docker compose up -d # start docker compose down # stop docker compose up -d --build # rebuild after code changes docker compose restart # restart without rebuild docker compose logs -f # follow logs ``` --- ## Exposing via Caddy + Authentik The app has no built-in authentication. For external access, put it behind a reverse proxy with forward auth. ### DNS Add an A record: `travel.yourdomain.com` → your Caddy LXC IP. ### Authentik 1. Create a **Proxy Provider** (Forward auth, single application) - External host: `https://travel.yourdomain.com` 2. Create an **Application** pointing at the provider 3. Add the application to your existing **Outpost** 4. Optionally add a policy to restrict access to specific users or groups ### Caddyfile ```caddy travel.yourdomain.com { header { Strict-Transport-Security "max-age=31536000;" X-Content-Type-Options "nosniff" X-Frame-Options "DENY" Referrer-Policy "strict-origin-when-cross-origin" } forward_auth http://:9000 { uri /outpost.goauthentik.io/auth/caddy copy_headers X-Authentik-Username X-Authentik-Groups X-Authentik-Email X-Authentik-Uid trusted_proxies private_ranges } reverse_proxy http://: } ``` ### Port binding When running behind a reverse proxy, bind Docker to the Caddy LXC IP only (already set in `docker-compose.yml`): ```yaml ports: - "127.0.0.1:${PORT:-8000}:8000" ``` If Caddy runs on a different host than the Travel Monitor LXC, change `127.0.0.1` to the Caddy LXC's IP. --- ## Security notes - The app has no authentication of its own — rely on the reverse proxy layer - API keys are stored in `.env` on the host filesystem — secure the LXC accordingly - The `simulate-delay` endpoint is a debug tool — unlock it in the UI by typing `thisisdebug`; it is not accessible from the browser by default and is only useful for verifying threshold logic - All API endpoints are rate-limited to prevent API quota exhaustion --- ## Hardening checklist - [x] Rate limiting on all endpoints (slowapi) - [x] Docker port bound to localhost / Caddy IP only - [x] No user input executed or evaluated server-side - [x] XSS protection via `escHtml()` on all user-supplied strings in the frontend - [ ] Caddy security headers (add to Caddyfile block) - [ ] Authentik group policy restricting access to your account only - [ ] Firewall rule on Travel Monitor LXC allowing only Caddy LXC on the app port --- ## TODO - [ ] Mobile layout polish (touch edge cases) - [ ] Authentik + Caddy deployment (documented above, not yet deployed)