Repository navigation
ADR 0058 Theft Tracking Mode
Accepted (2026-10-27)
ADR 0027 raises critical theft alarms (mower_lifted, mower_left_dock_unexpectedly, geofence_exit) with one position, and the Device-Gateway reports a docked or idle Mammotion mower only every few minutes. A thief who carries the mower away needs a different rhythm: a position every few seconds over the 4G link, a way to show the police where it is without handing over the account, and a report to attach to the complaint. The owner decided: tracking starts by itself on a theft alarm and ends by hand ("Fehlalarm / Ortung beenden") or after 24 h; positions every 10 s (both configurable on the server); a secret share link that shows only a live map for 48 h; a PDF theft report.
Telemetry owns the sessions. It already holds the position track, the mower directory and the mower routes, and it is the one service that reads positions; a new service for three tables would add a deployment, a database and CI jobs without a boundary worth drawing. Tables (migration 0012): tracking_sessions (a partial unique index allows one active session per mower), tracking_links (only the SHA-256 of the token), tracking_photos; mower_directory gains name and serial (the manufacturer's device name from the connection options, the closest to a serial number MowDeck knows). The session stores no positions: its track is the normal position track (mower_positions) between started_at and ended_at, which the gateway fills at the session's interval.
State machine. active until ended_at; end reasons false_alarm, stopped (by hand) and max_duration (MOWDECK_TELEMETRY_TRACKING_MAX_HOURS, 24). Starting is idempotent (a second alarm while tracking changes nothing). Start triggers: the three alarm kinds (Telemetry consumes notifications.raised, durable telemetry-notifications; MOWDECK_TELEMETRY_TRACKING_AUTO_START switches it off) or POST /mowers/{id}/tracking by hand. A background tick every MOWDECK_TELEMETRY_TRACKING_TICK_SECONDS (15) runs on one replica (PostgreSQL advisory lock) and, per active session: ends it at the maximum, follows the position, marks it stale when the mower has not reported for ..._STALE_AFTER_SECONDS (120) or the link is down (the last known position stays; one notice per outage), notices a move of at least 50 m at most every 5 minutes, and renews the gateway lease.
Gateway lease, not a command. Telemetry asks the Device-Gateway that holds the mower, over core NATS gateway.mower.<id>.tracking (TrackingLeaseRequest/Reply), for reports every interval_seconds (10, never below 5) for lease_seconds (60), and renews it on every tick. When Telemetry dies the lease runs out and the mower returns to its normal rhythm, so a crash can never leave a mower polling the vendor cloud for ever. The session applies it through the new optional connection capability TrackingRateCapable (like ReportRateCapable, ADR 0042); the Mammotion adapter gets a track mode in its refresh loop: one undebounced report request per interval (cloud/4G or Bluetooth, whatever link is up), and the gateway heartbeat follows the interval so every report is a track point. A connection that reports on its own (the simulator) needs no port. Replies: ok, not_found (also for a foreign account), not_connected (the next renewal applies it when the mower is back).
Vendor limits. About 0.1 requests per second per tracked mower is far below the 1 per second of manual driving that the vendor server accepted (ADR 0049). The adapter additionally backs off: three consecutive polls without a new report double the wait (cap 120 s), a failed request counts as silent and is logged at warning level (the first and every tenth), a report resets it. The lease is the only way to start it, and it ends by itself.
Notifications. Telemetry publishes tracking.started, .moved, .stale, .ended (event.tracking.*, TrackingEvent); Notifications consumes them (durable notifications-tracking) and creates the kinds tracking_started (critical), tracking_moved, tracking_stale (warning) and tracking_ended (info). All but the end ignore quiet hours; they have no acknowledge silence (the end of the session is the answer) and no cooldown of their own: Telemetry sends no spam (one start, a move at most every 5 minutes, one stale notice per outage, one end). They open /mowers/<id>/tracking; ending a session from the notification itself needs a service worker action and is not part of this change (the page opens with the "false alarm" button one tap away).
Rights follow ADR 0027 and need no model change: reading the session, the links and the report need can_view; starting, ending, taking photos, creating and revoking links and deleting need can_control (a viewer must not publish the location). Acknowledging an alarm needs only the inbox item.
Share link. The owner creates a link for the active or newest session: 256 bits from the CSPRNG, shown once, only its SHA-256 stored, expiry 48 h (..._LINK_TTL_HOURS; a caller may ask for less, never more), at most 10 active links per session, revocable at once, listed with created/expires/uses in the tracking view. The address is /track#<token>: the token sits in the URL fragment, so it reaches no access log, proxy or Referer, and the page strips it from the address bar. The page calls POST /api/v1/telemetry/public/tracking/view (token in the body, no login) and gets exactly: mower name and model, active or ended, start and end time, last position, stale since, the track since the alarm (at most 1500 points), the link's expiry and the server time. No account, no mower id, no serial, no controls, nothing about other mowers. Unknown, expired and revoked tokens are all 404. Rate limit per client (rightmost X-Forwarded-For, else the peer): 60 requests per minute and 20 unusable tokens per minute, then 429 with Retry-After; the limiter is per replica and in memory (the token cannot be guessed, the limit protects the database). Every public answer carries Cache-Control: no-store, X-Robots-Tag: noindex, nofollow, noarchive, Referrer-Policy: no-referrer and a default-src 'none' CSP; the page itself is served by nginx with the same robots and referrer headers and no-store. A link outlives the end of the session until it expires (the police can look at the final track); it dies with the session's deletion.
Theft report. GET /mowers/{id}/tracking/report.pdf?lang=de|en builds a PDF on the server with fpdf2 (pure Python, LGPL-3.0, which is compatible with the AGPL; it brings defusedxml and fonttools and uses Pillow, already in the lock file) and its built-in Helvetica: mower model, serial/device name, MowDeck name, alarm kind and time, last position, the track as vectors on a blank metre grid (no map tiles are fetched or embedded, nothing leaves the server), a position table (at most 45 rows, thinned), up to four camera photos, and blank owner fields (name, address, contact, police report number, insurance). Photos: Telemetry asks for one still image at the start and once more a minute later if the first failed (gateway.mower.<id>.photo with the error-analysis demand, which may wake a sleeping camera through the relay and ignores the Vision budget, ADR 0045; a lifted mower is an error state), plus POST .../tracking/photo by hand (at most 5 per session). It stores its own images in tracking_photos; the camera photo of push notifications (ADR 0056) is a separate store and is not reused.
Privacy and retention. Ended sessions with their links and photos are deleted ..._RETENTION_DAYS (30) after the end by the existing hourly retention job; DELETE .../sessions/{id} deletes an ended session at once. The positions stay in the mower's position track under its own retention (90 days, ADR 0017); a mower removal deletes everything of the mower.
UI. /mowers/{id}/tracking (phone first: large map, status, "Link teilen", "Bericht (PDF)", "Ortung beenden" with the choice false alarm or stop, links list), the public page /track, a shield button and banner on the mower page, German and English texts. The tracking view polls the API every 8 s with after= (no new realtime message); the mower page every 30 s.
- The track depends on the gateway delivering reports: a mower out of coverage shows "stale since" and its last position, the track resumes when it reconnects. Without RTK the positions can be metres off; the report says so.
- The vendor cloud sees one report request per 10 s per tracked mower for at most 24 h (default). Lowering
interval_secondsraises that; the adapter floor is 5 s. - Share links are bearer secrets: whoever has the address sees the position until it expires or is revoked. The fragment keeps it out of logs, but not out of the chat where the owner pastes it.
- The rate limiter is per replica. Several Telemetry replicas multiply the allowance, which is still harmless for 256-bit tokens.
- The end-to-end test starts a session by hand (the same path an alarm takes); the lift itself cannot be triggered in the Compose stack, so alarm to session is covered by the Notifications and Telemetry integration tests on the bus.
- Not covered: ending a session from the notification (service worker action), mapping the vendor's real serial number, a realtime message instead of polling, photos on the share page.
Mirrored from docs/adr/0058-theft-tracking-mode.md at commit f2b638806535ae0bd3f33714e176f58bf05f35f2. Edit the file in the repository, not this wiki page.
Mirrored from the docs/ directory at commit f2b638806535ae0bd3f33714e176f58bf05f35f2. Edit the files in the repository, not this wiki.
MowDeck documentation
- Home
- Phases
- Architecture
-
Decisions (ADR)
- Architecture decisions
- ADR 0001: OpenFGA instead of self-built authorization
- ADR 0002: Self-built Identity service in FastAPI instead of Keycloak or Zitadel
- ADR 0003: SvelteKit for the web app
- ADR 0004: Monorepo with a uv workspace
- ADR 0005: AGPL-3.0 for services and apps, Apache-2.0 for libraries
- ADR 0006: PostgreSQL for all persistence, no SQLite
- ADR 0007: NATS JetStream as event bus
- ADR 0008: Traefik as gateway
- ADR 0009: Command and event envelopes, subject naming and de-duplication
- ADR 0010: Python 3.13 for all services and libraries
- ADR 0011: Separate GPL-3.0 package for the PyMammotion adapter
- ADR 0012: Mower control interface and safety rules
- ADR 0013: Device-Gateway registry, sharding and per-mower concurrency
- ADR 0014: Authorization model, tuple ownership and the mowdeck-authz client
- ADR 0015: Tokens, sessions and key rotation
- ADR 0016: Fleet commands and authorization per command kind
- ADR 0017: Telemetry storage and retention
- ADR 0018: Realtime WebSocket protocol
- ADR 0019: Mammotion adapter transports, manual drive and secrets
- ADR 0020: Web app architecture
- ADR 0021: Browser authentication flow
- ADR 0022: Notifications: rules, recipients and delivery
- ADR 0023: Map rendering and projection
- ADR 0024: Scheduler: recurrence, rain pause and command issuing
- ADR 0025: Home Assistant integration as a pure API client
- ADR 0026: Manual drive channel over the Realtime WebSocket
- ADR 0027: Health history and theft alarm
- ADR 0028: Entitlements: plans as data and enforcement
- ADR 0029: Helm chart and release pipeline
- ADR 0030: Mowing patterns as plug-ins and the Planner
- ADR 0031: Billing with a Stripe adapter
- ADR 0032: Live camera over Agora, joined directly by the browser
- ADR 0033: Vision-assisted recovery with human approval
- ADR 0034: Linked cloud accounts and device discovery
- ADR 0035: Server-side camera frames and automatic AI analysis on errors
- ADR 0036: Native app shells with Capacitor, loading the web app from the server
- ADR 0037: Camera panorama calibration in the browser, with the AI supplying points only
- ADR 0038: Dual link per mower and firmware updates
- ADR 0039: Spot mowing by MowDeck-driven paths
- ADR 0040: Link heartbeat for online status
- ADR 0041: Spot runs from the dock: undock, transit and return
- ADR 0042: Drive calibration
- ADR 0043: Turn styles
- ADR 0044: Drive view with camera
- ADR 0045: Camera relay
- ADR 0046: Device settings as capabilities
- ADR 0047: The RTK base station as a Fleet device
- ADR 0048: Maintenance commands
- ADR 0049: Manual driving over the Mammotion cloud
- ADR 0050: Passkeys and the app login hand-off
- ADR 0051: Sign in with GitHub
- ADR 0052: Sign in with Apple
- ADR 0053: Retention and downsampling of the position track
- ADR 0054: Aerial photo under the mower map
- ADR 0055: Deploy maintenance window
- ADR 0056: Camera photo in push notifications for errors and theft alarms
- ADR 0057: Step-up re-authentication for critical actions
- ADR 0058: Theft tracking mode
- ADR 0059: iOS widgets and the mowing Live Activity
- ADR 0060: Apple Watch app
- Development
-
Operations
- Operating the Compose stack
- Operating MowDeck on Kubernetes (Helm)
- Building the Bluetooth bridge (ESPHome on a XIAO ESP32-S3)
- Releases
- Billing operations
- Telemetry: retention and position downsampling
- iPhone and Android apps
- Passkeys
- Sign in with GitHub
- Aerial photo under the map
- Sign in with Apple
- Theft tracking mode
- Source code offer (AGPL-3.0 section 13)
- Spikes
- Guides
- Original plan (German)