Repository navigation
ADR 0062 Setup Tour
Accepted (2026-10-28)
The owner wants a guided setup for new users ("Onboarding-Tour: geführte Einrichtung für neue Nutzer"): after the first sign-in, and again on request, walk the user to a working MowDeck. Connect the Mammotion account and add the mower, check that the map arrived, set up notifications, add a passkey, optionally link a sign-in, create the first schedule, and inside the iPhone app turn on the widgets and the Apple Watch. Each step shows whether it is done, can be skipped, and the tour can be resumed. Short "first time here" hints on the main screens (mower map, schedules) disappear once dismissed.
Forces:
- Every step already has its flow: the linked accounts section (ADR 0034),
/mowers/new, the mower page with its map, the notification settings with Web Push, APNs and ntfy, the passkeys panel (ADR 0050), the provider links (ADR 0051, ADR 0052),/schedules/new, the widgets panel of the app (ADR 0059, ADR 0060). The tour must link them, not duplicate them (the install hint for iOS browsers stays where it is). - "Done" must come from the real state, not from a checkbox the user ticks: a mower exists, Telemetry has its map, a push channel is registered, a passkey exists, a schedule exists.
- The progress should follow the user from the browser to the iPhone app. Identity had no per-user preferences so far; the web app keeps device-local flags (
mowdeck.whats-new-seen, the install hint) inlocalStorage. - Existing users (the owner) must not be bombarded: their accounts are set up already.
- Many e2e specs register fresh accounts and drive the mower list; a modal tour on top of it would block them and the user alike.
Identity keeps only the user's own choices, per user, in a new table onboarding_progress (migration 0006, one row per user, removed with the user): status (active, dismissed, completed; no row means new), skipped_steps and dismissed_hints (JSON lists of keys). GET and PUT /api/v1/identity/me/onboarding read and replace it; only users and sub-users have a tour (API tokens 403). The keys belong to the web app: Identity checks only their shape (^[a-z][a-z0-9-]{0,31}$, at most 32 each), so a new step needs no server change. The web app keeps keys it does not know when it saves (another app version may have written them). No audit entries: these are display preferences.
Server side rather than localStorage, because the tour is about the account, not the device: a user who skips the passkey step in the browser should not be asked again in the iPhone app, and a dismissed hint should stay dismissed everywhere. A dedicated table rather than a generic preferences store: there is exactly one consumer, and a typed table keeps the contract (and its validation) explicit; a generic store can absorb it later.
Whether a step is done is never stored. The web app reads it every time from the services:
| Step | Applies when | Done when |
|---|---|---|
cloud-account Mammotion account |
the user has fleet.manage and a connection kind takes mowers from a linked account |
Fleet lists a linked account, or the user sees a mower (a mower of another kind needs no cloud account) |
mower Add the mower |
fleet.manage |
Fleet lists a mower |
map Check the map |
always | Telemetry answers 200 for the map of one of the first three mowers |
notifications |
the notification settings are visible | a Web Push subscription, an iOS (APNs) device or an ntfy target exists |
passkey (optional) |
passkeys are configured (/login-methods) |
the user has a passkey |
schedule |
the Scheduler is deployed and visible | a schedule exists |
sign-in-links (optional) |
GitHub or Apple sign-in is configured | a provider is linked |
app-extras (optional) |
inside the iOS app with widget support | the widgets are on on this iPhone |
A fact that cannot be read is unknown: the step stays open, it is never counted as done. The tour is complete when every required step is done or skipped; optional steps never hold it up.
-
A new account that may add mowers and has none (status
new, no mower,fleet.manage): the mower list shows the setup card, and the tour starts by itself (statusactiveis stored on the first visit). -
An account set up before the tour existed (status
new, at least one mower): one slim, dismissible line ("Einrichtungshilfe: prüfen …") on the mower list; dismissing it storesdismissed. Nothing else. -
dismissed/completed: nothing on the mower list. Settings → Einrichtung (for every user) shows the status and opens the tour again; opening it on purpose resumes a dismissed tour. -
First-time hints (
mower-map,schedules) show only to users who started the tour (activeorcompleted), each until dismissed. Accounts set up before the tour never see them. - "Was ist neu" is unchanged: the card sits in the page flow at the top and the sheet is non-modal, so closing the sheet leaves the card as it was.
- The setup card sits in the page flow of the mower list (like the install hint), never as an overlay: the list, the add button and the emergency stop stay usable. It shows the progress scale (one segment per step: filled done, hatched skipped, hairline open, the count in mono), the next open step with its action into the existing flow, "Tour öffnen" and "Später".
-
/setupis the tour itself: a numbered checklist with the state of every step; the next open step is expanded (what it is for, its action, "Überspringen"), a skipped one offers "Wieder aufnehmen". The facts are read again when the page comes back into view (after a flow in another tab or app). When the last required step settles, a completion panel takes the focus; "Fertig" storescompleted. - Accessibility: steps are disclosure buttons with
aria-expandedandaria-controlsinside headings; after a skip the focus moves to the next open step; the progress scale is announced as a sentence ("3 von 7 erledigt"), its segments are hidden from screen readers; motion is limited to short fades and off withprefers-reduced-motion. - Mobile-first: the checklist is one column on phones, the panel takes the full width under its step; targets are at least 48 px.
- One small table and two endpoints in Identity; the web app reads the facts with up to about ten small requests when the tour or the mower list card needs them (not for dismissed or completed tours, which only load the choices).
- A new step is a web app change only (definition, texts, a probe). A step key removed from the web app stays harmlessly in the stored lists.
- The e2e suite covers a new account, skip, resume in another browser, completion from the real state, the first-time hints and the one-line hint for an account set up before the tour (
tests/e2e-web/tests/onboarding-desktop.spec.ts). - Not covered: the native-only step is tested at the unit level (the e2e suite runs in browsers); facts that change in another tab are picked up when the tour page comes back into view, not live.
-
localStorageonly (like "Was ist neu"): no server change, but the tour would restart on every new device and in the iPhone app, and hints would come back after clearing site data. - Store "done" per step on the server: stale as soon as something changes elsewhere (a mower deleted, a passkey removed), and a second source of truth next to the services.
- A modal tour over the app (coach marks): blocks the very flows it points to and every e2e spec that starts on an empty mower list; a page plus an in-flow card is calmer and works the same with a screen reader.
- Show the tour to every account without a stored status: would bombard existing, set-up accounts; they get one dismissible line instead.
Mirrored from docs/adr/0062-setup-tour.md at commit d31bb5cc2fa17ace35f89531465b76d8f888ece6. Edit the file in the repository, not this wiki page.
Mirrored from the docs/ directory at commit d31bb5cc2fa17ace35f89531465b76d8f888ece6. 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
- ADR 0061: Mowing angle rotation
- ADR 0062: Setup tour for new users
- ADR 0063: Matter server (mowers in Apple Home and Google Home)
- ADR 0064: Personal access tokens and the public API documentation
- 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
- Matter server
- Source code offer (AGPL-3.0 section 13)
- Spikes
- Guides
- API reference
- Original plan (German)