Location service

Evan's personal location service at https://location.evanrthomas.com. The iPhone (Overland app) posts GPS fixes; the service stores them, answers "where is Evan / where was he", and fires subscriptions (geofence enter/exit, "landed" after a flight, "stale" when the phone goes quiet) as signed webhooks or OpenClaw agent turns that end up in Evan's Telegram.

Doc What's in it
Set up Overland on iPhone URL to paste, where the token goes, recommended settings
API reference Every endpoint with curl examples
Subscriptions & callbacks Rules, event payloads, retries, signature verification
OpenClaw integration How events become Telegram messages; the location skill
Swapping trackers Adding a new source adapter (one file)
Operations runbook Deploy, logs, restart, rotate tokens, backups, DNS

Interactive API explorer (Swagger): /api-docs · OpenAPI JSON: /openapi.json

Architecture

One small Python service (FastAPI + SQLite). The "layers" are modules, not network services.

 iPhone: Overland ──HTTPS POST /ingest/overland──┐        (OwnTracks HTTP mode → /ingest/owntracks)
                                                 ▼
 ┌───────────── OpenClaw EC2 box (34.198.157.4) ──────────────────────────────────────────────┐
 │  Caddy :443  location.evanrthomas.com (Let's Encrypt, no Google login)                     │
 │     │                                                                                      │
 │     ▼                                                                                      │
 │  location-service  127.0.0.1:18810  (systemd, user `location`)                             │
 │     sources/overland.py, sources/owntracks.py ──► LocationFix {lat, lon, accuracy_m,       │
 │                                                   timestamp, battery, motion, source,device}│
 │     processing.py ──► db.py (SQLite /var/lib/location-service/location.db)                 │
 │          │              fixes · subscriptions(+state) · events(= delivery queue)           │
 │          └──► events.py (pure): geofence entered/exited · landed · stale                   │
 │     delivery.py (retry loop) ──► webhook callback_url (HMAC-signed)                        │
 │                              └─► OpenClaw  POST 127.0.0.1:18789/hooks/agent                │
 │                                      └─► agent turn ──► Telegram (Evan)                    │
 │  OpenClaw agent ── `location` skill ── curl 127.0.0.1:18810 (API token) ───────────────────┘
 └────────────────────────────────────────────────────────────────────────────────────────────┘

Code map (backend/):

File Role
main.py Entry point: cfg = config.Config.from_env(), runs uvicorn (single worker)
config.py Pydantic Config; every field is env var LOCATION_<NAME>
app.py FastAPI routes, token auth, /docs markdown rendering
sources/*.py Tracker adapters → schemas.LocationFix
events.py Pure detection functions (no I/O)
processing.py Ingest + rule evaluation (serialized by a lock), stale loop
delivery.py Webhook / OpenClaw request building, HMAC, retry loop
db.py / models.py / schemas.py Storage functions / ORM tables / pydantic objects
geocode.py Best-effort reverse geocode (Nominatim) for landed

Infra: DNS in terraform/ (A record → OpenClaw EIP), deploy in scripts/deploy.sh + deploy/. The box itself is managed in ethomas2/openclaw-ec2 (see its INFRA.md).