214 lines
7.6 KiB
Markdown
214 lines
7.6 KiB
Markdown
# 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@<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
|
||
|
||
```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://<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`):
|
||
```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)
|