Repository navigation
ADR 0073 Guided Tour
Accepted (2026-11-01). Extends ADR 0062 (setup tour) and revisits its rejected alternative "a modal tour over the app".
The owner wants an interactive, step-by-step tour of the web app: short notes that point at the real controls (spotlight and coach marks, next/back/skip, progress) for the core of MowDeck: the mower list, the mower page (live map, start/pause/dock, spot mowing), schedules, zones, notifications and settings. It starts by itself once for a new user, can be started again any time, works in the public demo (ADR 0068) and on phones and desktops, and is fully keyboard and screen reader accessible.
What exists:
- The setup tour of ADR 0062 is a checklist of things to do (link the account, add the mower, push, passkey, first schedule), shown as an in-flow card on the mower list and the
/setuppage. It deliberately rejected "a modal tour over the app (coach marks)" because such a tour "blocks the very flows it points to and every e2e spec that starts on an empty mower list". - Identity stores the setup tour's choices per user (
GET/PUT /api/v1/identity/me/onboarding): a status, skipped steps anddismissed_hints, a list of opaque keys the web app owns (^[a-z][a-z0-9-]{0,31}$, at most 32). A new key needs no server change. - Features are gated by the mower's capabilities (
GET /telemetry/mowers/{id}/capabilities, ADR 0012), by permissions and by the modules the deployed services announce (the shell registry). - The demo shares one user between every visitor and locks what the services refuse (
DemoLock).
The two tours answer different questions: the setup tour "what is still missing?", the guided tour "where is what?". The guided tour shows; it does not set anything up.
apps/web/src/lib/tour/: steps.ts (pure: the steps, the filters, moving over missed steps, the automatic start rule, the outcome keys), tour.svelte.ts (the state machine; navigation, DOM lookup, storage and services injected through TourDeps), runtime.ts (the wiring), TourOverlay.svelte (the overlay) and labels.ts (texts). It is mounted once in the (app) layout next to "What's new".
Libraries considered: driver.js (MIT, small, framework free) would do the highlighting, but its popover markup and styles would be replaced anyway to fit the instrument panel design system, its focus handling would have to be extended for a focus trap and live announcements, and steps that live on other pages, wait for their element or are filtered by capability are not its model; intro.js is AGPL-3.0 with a paid commercial licence and brings its own look. Everything the tour needs (one fixed overlay, one rectangle, one card, a step machine) is a few hundred lines that use the existing tokens, Button and Paraglide; no new dependency.
| Step | Page | Points at (data-tour) |
Shown when |
|---|---|---|---|
welcome |
where the user is | nothing (centred card) | always |
overview |
/mowers |
the mower list | the user sees a mower |
map |
/mowers/{first} |
the map section | the user sees a mower |
commands |
/mowers/{first} |
the command bar with the emergency stop | the user sees a mower |
spot |
/mowers/{first} |
"Spot mähen" | a mower whose capabilities have manual_drive (spot runs drive the mower, ADR 0039) |
zones |
/mowers/{first} |
the areas list, or the zone editor link (ADR 0070) | the user sees a mower |
schedules |
/schedules |
the schedule list or its empty state | a mower, and the schedules module is visible |
inbox |
/notifications |
the inbox or its empty state | a mower, and the notifications module is visible |
settings |
/settings |
the numbered settings index | the user sees a mower |
restart |
/settings#setup |
"Start the tour" | always |
add-mower |
/mowers |
the empty mower list | the user sees no mower: the last step, its button leads to /mowers/new (for a user with fleet.manage; a viewer reads that a mower will appear) |
Filters are declared on the step (applies(context)) and read from the provider neutral sources: the first mower's capabilities, fleet.manage, the visible modules. Nothing names a vendor. The mower steps use the first mower of the list.
A missing element never blocks. A step's element is looked up for up to 6 s after the page opened ([data-tour="..."], first visible match). If it does not appear (the page failed to load, a mower without areas or zones, a layout that leaves it out), the step is marked missed for this run and the tour moves on in the direction of travel; Back passes over it too, and the progress counts only the steps still shown. If the element leaves the page while shown, it is looked up again.
The tour starts by itself only on /mowers (the landing page after the sign-in, never over a deep link), not while "What's new" is open, once per session, and only when:
- the stored outcome is
new(never seen), and - the user is new in the sense of ADR 0062: the setup tour's status is
active, ornewwith no mower. An account set up before the tours existed is not bombarded; Settings → Setup starts it.
In the demo every new browser is a new visitor (below).
| What | Where | Why |
|---|---|---|
Outcome: tour-seen (opened, not finished), tour-done, tour-skipped
|
Identity, the user's dismissed_hints (ADR 0062) |
Per user, follows them to every device and the app shells; existing table and endpoint, no server change. The web app replaces only its own keys and keeps unknown ones. |
| Outcome in the demo |
localStorage mowdeck.tour.demo
|
Every visitor shares the demo user: a server-side outcome would let the first visitor end the tour for everyone. |
| The running step |
sessionStorage mowdeck.tour.run (with the user's subject) |
A reload in the middle continues the tour in that tab; nothing to keep beyond it. |
| Device switch |
localStorage mowdeck.tour.autostart = off
|
Never start by itself on this device: wall displays, kiosks and the e2e suite. Starting it from Settings still works. |
tour-seen is written when the tour opens, so a tour closed with the tab does not start again. Finishing (done) also dismisses the setup tour's first-time hints on the mower map and the schedules (the tour explained them). A setup tour status new is saved as what the mower list would make of it (active for a new account that may add mowers and has none, otherwise dismissed), because Identity has no new to store; this hides the one-line "check the setup" hint of an old account that takes the guided tour, which is the same answer. Taking the tour again never turns done into skipped.
-
Viewfinder, not a glow. The framed element sits in a hole of the dimmed page (
--md-scrim), marked with four corner brackets in HUD ink and the step number as a mono tag, like a callout on an instrument drawing. The coach card is a plain surface with the ink top rule of the sheets, an uppercase label, a mono counter ("03 / 10") and a tick scale (past, now, ahead), the step's title and text, and in the demo a note with the demo band's hatched tape. Tokens only, light and dark from the same tokens. - Placement. Below 768 px the card docks to the top or bottom edge, whichever is away from the framed element (the command bar at the bottom gets the card at the top). Wider, it sits below or above the element, beside a screen-tall one, clamped into the viewport.
-
Never moves the page. Fixed layers only, no scroll lock, no inserted elements in the flow: no layout shift. The element is scrolled into view once (
smooth,autowith reduced motion). - Nothing is triggered. A transparent layer catches every click on the page while the tour runs, the framed element included. In the demo the notes explain what is locked; the tour itself never sends a command or opens a locked flow.
-
Keyboard and screen readers. A
role="dialog"witharia-modal, labelled by the step title and described by its text; the focus moves into the card (the primary button) and stays there (Tab wraps, focus that leaves is brought back);→next,←back,Escleaves the tour (stored as skipped); a polite live region announces "Step 3 of 10: The live map"; the counter's visual form is hidden from screen readers in favour of the sentence; on closing the focus returns where it was. Targets are 48 px. -
Motion. The frame glides between targets and the card rises once (
--md-motion-base); both off withprefers-reduced-motion.
Settings → Setup (#setup, for every user) has a "Guided tour" panel with the outcome and "Start the tour", above the setup tour's panel. The tour's own restart step points there.
All texts are Paraglide messages (tour_*) in German and English; the i18n check covers them. Step texts are short (one or two sentences), German first.
- No new dependency, no server change: Identity already stores opaque keys. A new step is a definition in
steps.ts, two texts and adata-tourattribute on the page. - Pages carry
data-tourattributes on the elements the tour points at (overview,add-mower,map,commands,spot,zones,schedules,inbox,settings,tour-restart). Removing one does not break the tour (the step is passed over) but the e2e specs notice. - The e2e suite runs with the device switch off for every context (
storageStateinplaywright.config.ts, which Playwright also gives tobrowser.newContext()), so specs that sign in fresh accounts are not covered by a modal tour. The tour specs start with a clean browser:tour-desktop.spec.ts(automatic start once, leading to "add mower", Esc stored as skipped, start from Settings, arrow keys over the mower screens with the focus kept in the card, a step whose page fails to load passed over in both directions),tour-phone.spec.ts(390 px: docking away from the target, no sideways overflow, Esc),tour-demo.spec.ts(projectsdemoanddemo-phone: first visit of a browser, the demo notes, a second browser gets it again). Unit tests:steps.test.ts(filters with property tests, moving over missed steps, outcome keys, the automatic start rule),tour.svelte.test.ts(the state machine with fakes) andtour-overlay.test.ts(dialog semantics, keys, focus trap, demo notes, the last step). - The tour reads the mower list and one capability set when it starts (two small requests); the automatic start also loads the setup tour's choices, which the mower list loads anyway.
- Not covered: a mower page that the user changes during the tour (the tour shows the first mower); steps for the iOS app's native extras (ADR 0062's setup tour covers them).
- A tour library (driver.js, intro.js): see above; restyling and extending one costs about as much as the component, and adds a dependency to keep up to date, or a licence that does not fit.
-
localStorageonly for the outcome: no request, but the tour would start again on every device and in the app shells; kept only for the demo, where the user is shared. - Non-modal coach marks (the page stays clickable): would let a click on a highlighted command start the mower in the middle of a tour, and leave keyboard and screen reader users without a clear context. The modal overlay with Esc always one key away, and the e2e device switch, answer ADR 0062's concerns instead.
- Start for every account without a stored outcome: would start a tour over the owner's long set-up installation after an update; ADR 0062's rule for "new" is reused instead.
Mirrored from docs/adr/0073-guided-tour.md at commit ccac8bf3da8a6f86155c29f6cd1aad963968fb68. Edit the file in the repository, not this wiki page.
Mirrored from the docs/ directory at commit ccac8bf3da8a6f86155c29f6cd1aad963968fb68. 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
- ADR 0065: Security center and new-login alerts
- ADR 0066: Cloud provider plugins and adapters for more vendors
- ADR 0067: Error code lexicon, provider independent
- ADR 0068: Public demo mode
- ADR 0069: Provider conformance kit and provider package template
- ADR 0070: Virtual zones for mowers without a vendor map
- ADR 0071: Capability matrix generated from the adapters
- ADR 0072: API explorer in the web app, for the public demo
- ADR 0073: Guided tour with coach marks
- ADR 0074: NATS authentication and per-service subject permissions
-
Development
- Getting started
- Service template
- Shared libraries
- Cloud providers: adding a vendor
- Adding a provider: template and conformance kit
- Adding an error lexicon for a vendor
- Design: identity and authorization
- Design: fleet, telemetry, realtime
- Design: web app
- Web app
- Home Assistant integration
- Camera source bridge protocol
- Fuzzing and property-based tests
- GitHub wiki mirror
-
Operations
- Operating the Compose stack
- Operating MowDeck on Kubernetes (Helm)
- Building the Bluetooth bridge (ESPHome on a XIAO ESP32-S3)
- Releases
- Vendor library updates
- 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)
- Public demo
- Load testing
- Spikes
-
Guides
- Guides
- ESP32 bridge: purchase and setup
- Signing in with a passkey
- Signing in with GitHub
- Error codes in plain words
- Aerial photo under the map
- Signing in with Apple
- Confirming critical actions
- Your security page
- iPhone widgets and the mowing Live Activity
- Apple Watch
- The setup tour
- Apple Home and Google Home (Matter)
- Worx Landroid, Kress and LandXcape mowers
- Scripting MowDeck
- Capability matrix
- API reference
- Original plan (German)