7.6 KiB
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
thisisdebuganywhere on the page to unlock traffic simulation tools - Keyboard shortcuts —
Rrefresh,1–9jump to location,Tabswitch tabs,?show shortcuts,Escclose - 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
# 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@<LXC_IP>:/opt/travel-monitor
# 3. SSH in and configure
ssh root@<LXC_IP>
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://<LXC_IP>:<PORT> 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
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
- Create a Proxy Provider (Forward auth, single application)
- External host:
https://travel.yourdomain.com
- External host:
- Create an Application pointing at the provider
- Add the application to your existing Outpost
- Optionally add a policy to restrict access to specific users or groups
Caddyfile
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://<AUTHENTIK_OUTPOST_IP>: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://<TRAVEL_MONITOR_LXC_IP>:<PORT>
}
Port binding
When running behind a reverse proxy, bind Docker to the Caddy LXC IP only (already set in docker-compose.yml):
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
.envon the host filesystem — secure the LXC accordingly - The
simulate-delayendpoint is a debug tool — unlock it in the UI by typingthisisdebug; 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
- Rate limiting on all endpoints (slowapi)
- Docker port bound to localhost / Caddy IP only
- No user input executed or evaluated server-side
- 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)