Keep the inbox tour on screen and in step with the user #298

Merged
robbertbos merged 1 commit from tour-waits-for-async-banners into main 2026-08-13 09:23:22 +00:00
Owner

Closes #289, plus three defects found while testing that fix.

A coach-mark is a top-layer bubble anchored to a live element, which is where all four problems come from.

It started too early. The banners App.vue renders above the router view hang off /api/auth/me and /api/accounts; arriving late, they dragged an open bubble down the page (#289). The tour now waits for both. The retention banner needed nothing - it lands with the settings query tour.active already waits for.

A click beside a callout ended it for good. NLDD reports that as reason ignored, and Waggle mapped it to ending the tour: hint marked seen, demo card gone with it, nothing left on screen to explain what happened. It now puts the same step back and leaves the counter alone. Advancing on it is wrong the other way round - every stray click walks the tour forward, and step 4 opens a card modal over whatever the user was doing.

One click counted as many steps. A card list mounts a coach-mark per row (25 cards = 50 of them, all inactive but the tour's own) and NLDD fires nldd-close on those too. The tour counted each one, raced to the end and dismissed itself. Hint now stays quiet unless it is the active coach-mark - the right rule for every consumer. Steps 1-3 live in the list and 4-7 in the card, so the tour also follows the user across that line instead of leaving the screen blank with the skip button still up.

The in-card steps were painted under the modal. Both live in the top layer, where the last promotion wins, so a step lit before showModal() sat under the dialog - present, visible to every locator, invisible to the user until a Tab re-promoted it. Modal already emits open after showModal(), so that is when they light up.

Testing

  • Inbox.test.ts, Hint.test.ts: the tour is held while me/accounts are pending; a click beside the callout keeps its step; a close outside a running tour does nothing; the tour follows the user in and out of a card; an inactive hint stays quiet.
  • onboarding-tour-start.spec.ts: delays /api/accounts and asserts the banner is already up when the tour starts; asserts a click beside the callout neither ends nor advances it; asserts the in-card callout is the topmost element via elementFromPoint - toBeVisible() passes either way and would not catch it.
  • Each was confirmed red against the behaviour it replaces.
  • attachments-lightbox.spec.ts stubbed GET /api/settings without hints_seen, so the tour ran through that whole file and competed for the focus those tests assert on; it only passed because the old code destroyed the tour on the first stray click. The stub now carries hints_seen.

Verified in the browser as well as in CI. Local gates: 1095 vitest, e2e 98 passed / 3 skipped, typecheck, eslint, pre-commit.

Closes #289, plus three defects found while testing that fix. A coach-mark is a top-layer bubble anchored to a live element, which is where all four problems come from. **It started too early.** The banners `App.vue` renders above the router view hang off `/api/auth/me` and `/api/accounts`; arriving late, they dragged an open bubble down the page (#289). The tour now waits for both. The retention banner needed nothing - it lands with the settings query `tour.active` already waits for. **A click beside a callout ended it for good.** NLDD reports that as reason `ignored`, and Waggle mapped it to ending the tour: hint marked seen, demo card gone with it, nothing left on screen to explain what happened. It now puts the same step back and leaves the counter alone. Advancing on it is wrong the other way round - every stray click walks the tour forward, and step 4 opens a card modal over whatever the user was doing. **One click counted as many steps.** A card list mounts a coach-mark per row (25 cards = 50 of them, all inactive but the tour's own) and NLDD fires `nldd-close` on those too. The tour counted each one, raced to the end and dismissed itself. `Hint` now stays quiet unless it is the active coach-mark - the right rule for every consumer. Steps 1-3 live in the list and 4-7 in the card, so the tour also follows the user across that line instead of leaving the screen blank with the skip button still up. **The in-card steps were painted under the modal.** Both live in the top layer, where the last promotion wins, so a step lit before `showModal()` sat under the dialog - present, visible to every locator, invisible to the user until a Tab re-promoted it. `Modal` already emits `open` after `showModal()`, so that is when they light up. ### Testing - `Inbox.test.ts`, `Hint.test.ts`: the tour is held while `me`/`accounts` are pending; a click beside the callout keeps its step; a close outside a running tour does nothing; the tour follows the user in and out of a card; an inactive hint stays quiet. - `onboarding-tour-start.spec.ts`: delays `/api/accounts` and asserts the banner is already up when the tour starts; asserts a click beside the callout neither ends nor advances it; asserts the in-card callout is the topmost element via `elementFromPoint` - `toBeVisible()` passes either way and would not catch it. - Each was confirmed red against the behaviour it replaces. - `attachments-lightbox.spec.ts` stubbed `GET /api/settings` without `hints_seen`, so the tour ran through that whole file and competed for the focus those tests assert on; it only passed because the old code destroyed the tour on the first stray click. The stub now carries `hints_seen`. Verified in the browser as well as in CI. Local gates: 1095 vitest, e2e 98 passed / 3 skipped, typecheck, eslint, pre-commit.
Start the inbox tour only once the banners above it have settled
Some checks failed
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 11s
security-scan / JS SCA (npm audit) (pull_request) Successful in 13s
CI / pre-commit (pull_request) Failing after 14s
security-scan / Python SAST (bandit) (pull_request) Successful in 16s
CI / backend-test (pull_request) Failing after 30s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 35s
test-build / build (frontend) (pull_request) Successful in 45s
test-build / build (backend) (pull_request) Successful in 46s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m23s
CI / e2e (pull_request) Successful in 3m37s
65d073147b
The coach-marks are anchored to live elements, so a banner that mounts above
them afterwards drags the open bubble down the page. The banners App.vue
renders above the router view hang off /api/auth/me and /api/accounts, so hold
the tour until both queries have settled; the retention banner already lands
with the settings query that the tour hint itself waits for.

Closes #289
robbertbos force-pushed tour-waits-for-async-banners from 65d073147b
Some checks failed
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 9s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 11s
security-scan / JS SCA (npm audit) (pull_request) Successful in 13s
CI / pre-commit (pull_request) Failing after 14s
security-scan / Python SAST (bandit) (pull_request) Successful in 16s
CI / backend-test (pull_request) Failing after 30s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 35s
test-build / build (frontend) (pull_request) Successful in 45s
test-build / build (backend) (pull_request) Successful in 46s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m23s
CI / e2e (pull_request) Successful in 3m37s
to a88fbd23b5
Some checks failed
CI / release-scripts (pull_request) Successful in 6s
security-scan / SBOM (trivy) (pull_request) Successful in 8s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 11s
security-scan / JS SCA (npm audit) (pull_request) Successful in 13s
CI / pre-commit (pull_request) Failing after 16s
security-scan / Python SAST (bandit) (pull_request) Successful in 17s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 36s
test-build / build (frontend) (pull_request) Successful in 45s
test-build / build (backend) (pull_request) Successful in 49s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m6s
CI / backend-test (pull_request) Successful in 2m38s
CI / e2e (pull_request) Successful in 3m55s
2026-08-12 21:07:57 +00:00
Compare
robbertbos force-pushed tour-waits-for-async-banners from a88fbd23b5
Some checks failed
CI / release-scripts (pull_request) Successful in 6s
security-scan / SBOM (trivy) (pull_request) Successful in 8s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 11s
security-scan / JS SCA (npm audit) (pull_request) Successful in 13s
CI / pre-commit (pull_request) Failing after 16s
security-scan / Python SAST (bandit) (pull_request) Successful in 17s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 36s
test-build / build (frontend) (pull_request) Successful in 45s
test-build / build (backend) (pull_request) Successful in 49s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m6s
CI / backend-test (pull_request) Successful in 2m38s
CI / e2e (pull_request) Successful in 3m55s
to 4dd85de8be
Some checks failed
CI / release-scripts (pull_request) Successful in 10s
security-scan / SBOM (trivy) (pull_request) Successful in 13s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 20s
security-scan / JS SCA (npm audit) (pull_request) Successful in 22s
security-scan / Python SAST (bandit) (pull_request) Successful in 23s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 43s
CI / backend-test (pull_request) Failing after 46s
test-build / build (backend) (pull_request) Successful in 1m11s
CI / pre-commit (pull_request) Successful in 1m11s
test-build / build (frontend) (pull_request) Successful in 1m3s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m16s
CI / e2e (pull_request) Failing after 1m24s
2026-08-12 21:17:41 +00:00
Compare
robbertbos force-pushed tour-waits-for-async-banners from 4dd85de8be
Some checks failed
CI / release-scripts (pull_request) Successful in 10s
security-scan / SBOM (trivy) (pull_request) Successful in 13s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 20s
security-scan / JS SCA (npm audit) (pull_request) Successful in 22s
security-scan / Python SAST (bandit) (pull_request) Successful in 23s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 43s
CI / backend-test (pull_request) Failing after 46s
test-build / build (backend) (pull_request) Successful in 1m11s
CI / pre-commit (pull_request) Successful in 1m11s
test-build / build (frontend) (pull_request) Successful in 1m3s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m16s
CI / e2e (pull_request) Failing after 1m24s
to d0fb45944d
Some checks failed
security-scan / SBOM (trivy) (pull_request) Failing after 9s
CI / release-scripts (pull_request) Successful in 10s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
security-scan / JS SCA (npm audit) (pull_request) Successful in 17s
CI / pre-commit (pull_request) Failing after 19s
security-scan / Python SAST (bandit) (pull_request) Successful in 20s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 38s
CI / e2e (pull_request) Has been cancelled
test-build / build (pull_request) Has been cancelled
CI / backend-test (pull_request) Has been cancelled
CI / frontend-test (pull_request) Has been cancelled
test-build / build (frontend) (pull_request) Has been cancelled
test-build / build (backend) (pull_request) Has been cancelled
2026-08-12 21:21:54 +00:00
Compare
robbertbos force-pushed tour-waits-for-async-banners from d0fb45944d
Some checks failed
security-scan / SBOM (trivy) (pull_request) Failing after 9s
CI / release-scripts (pull_request) Successful in 10s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 15s
security-scan / JS SCA (npm audit) (pull_request) Successful in 17s
CI / pre-commit (pull_request) Failing after 19s
security-scan / Python SAST (bandit) (pull_request) Successful in 20s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 38s
CI / e2e (pull_request) Has been cancelled
test-build / build (pull_request) Has been cancelled
CI / backend-test (pull_request) Has been cancelled
CI / frontend-test (pull_request) Has been cancelled
test-build / build (frontend) (pull_request) Has been cancelled
test-build / build (backend) (pull_request) Has been cancelled
to 6265a4cb82
Some checks failed
CI / release-scripts (pull_request) Successful in 8s
security-scan / SBOM (trivy) (pull_request) Successful in 11s
security-scan / JS SCA (npm audit) (pull_request) Successful in 17s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 17s
CI / pre-commit (pull_request) Failing after 19s
security-scan / Python SAST (bandit) (pull_request) Successful in 21s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 39s
test-build / build (frontend) (pull_request) Successful in 52s
test-build / build (backend) (pull_request) Successful in 54s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m0s
CI / backend-test (pull_request) Successful in 2m5s
CI / e2e (pull_request) Successful in 3m9s
2026-08-12 21:22:33 +00:00
Compare
robbertbos force-pushed tour-waits-for-async-banners from 6265a4cb82
Some checks failed
CI / release-scripts (pull_request) Successful in 8s
security-scan / SBOM (trivy) (pull_request) Successful in 11s
security-scan / JS SCA (npm audit) (pull_request) Successful in 17s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 17s
CI / pre-commit (pull_request) Failing after 19s
security-scan / Python SAST (bandit) (pull_request) Successful in 21s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 39s
test-build / build (frontend) (pull_request) Successful in 52s
test-build / build (backend) (pull_request) Successful in 54s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m0s
CI / backend-test (pull_request) Successful in 2m5s
CI / e2e (pull_request) Successful in 3m9s
to eb8f2a6a27
All checks were successful
CI / release-scripts (pull_request) Successful in 8s
security-scan / SBOM (trivy) (pull_request) Successful in 13s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 18s
security-scan / JS SCA (npm audit) (pull_request) Successful in 21s
security-scan / Python SAST (bandit) (pull_request) Successful in 23s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 41s
test-build / build (frontend) (pull_request) Successful in 1m10s
test-build / build (backend) (pull_request) Successful in 1m13s
test-build / build (pull_request) Successful in 0s
CI / pre-commit (pull_request) Successful in 1m13s
CI / frontend-test (pull_request) Successful in 1m21s
CI / backend-test (pull_request) Successful in 2m54s
CI / e2e (pull_request) Successful in 3m58s
2026-08-12 21:33:00 +00:00
Compare
Keep a click beside a coach-mark from ending the inbox tour
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 7s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 13s
security-scan / JS SCA (npm audit) (pull_request) Successful in 14s
security-scan / Python SAST (bandit) (pull_request) Successful in 15s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 37s
CI / pre-commit (pull_request) Successful in 50s
CI / frontend-test (pull_request) Successful in 55s
test-build / build (frontend) (pull_request) Successful in 1m26s
test-build / build (backend) (pull_request) Successful in 1m29s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 2m0s
CI / e2e (pull_request) Successful in 3m17s
cd7a5285ce
NLDD closes a callout with reason 'ignored' when the click lands next to it.
Waggle mapped that to ending the tour, which marks the hint seen: one stray
click and the tour was gone for good, taking the demo card with it, so nothing
on screen explained what had happened.

Put the same step back instead, and leave the counter alone. Stepping the tour
along on 'ignored' is wrong the other way round: every stray click walks it
forward, and step 4 opens the card modal over whatever the user was doing -
measured, and it is what the attachments-lightbox spec tripped over.

That spec stubs GET /api/settings without hints_seen, so every hint read as
unseen and the tour ran through the whole file; it only passed because the
first stray click destroyed the tour. The stub now carries hints_seen.
Let the inbox tour follow the user into a card, and stop counting stray closes
All checks were successful
CI / release-scripts (pull_request) Successful in 5s
security-scan / SBOM (trivy) (pull_request) Successful in 8s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 10s
security-scan / JS SCA (npm audit) (pull_request) Successful in 13s
security-scan / Python SAST (bandit) (pull_request) Successful in 14s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 37s
test-build / build (frontend) (pull_request) Successful in 46s
CI / pre-commit (pull_request) Successful in 50s
test-build / build (backend) (pull_request) Successful in 50s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 55s
CI / backend-test (pull_request) Successful in 2m0s
CI / e2e (pull_request) Successful in 3m8s
63b87ce0fd
A card list mounts a coach-mark per row - a card list of 25 carries 50 of them,
all inactive but the tour's own. NLDD fires nldd-close on those too, so a single
click produced a burst of them and the tour counted every one: it raced through
its steps, opened the demo card on the way and dismissed itself at the end. Hint
now stays quiet unless it is the active coach-mark, which is the right rule for
every consumer, not only the tour.

Steps 1-3 live in the list and 4-7 inside the card, so the tour also has to
follow the user across that line. Opening a card of your own on step 2 left the
tour with nothing on screen while the skip button still said it was running;
now the tour moves to the in-card steps, and closing the card puts it back on
the step that invites you in.
Light the in-card tour steps after the dialog, not before it
Some checks failed
CI / release-scripts (pull_request) Successful in 4s
security-scan / Filesystem scan (trivy fs) (pull_request) Failing after 5s
security-scan / SBOM (trivy) (pull_request) Successful in 10s
security-scan / JS SCA (npm audit) (pull_request) Successful in 16s
security-scan / Python SAST (bandit) (pull_request) Successful in 17s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 35s
test-build / build (frontend) (pull_request) Successful in 1m17s
test-build / build (backend) (pull_request) Successful in 1m21s
CI / pre-commit (pull_request) Successful in 1m23s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m25s
CI / backend-test (pull_request) Successful in 3m35s
CI / e2e (pull_request) Successful in 5m4s
247b575ccf
A coach-mark and the card modal both live in the top layer, where the element
promoted last paints on top. The tour lit its in-card step in the same tick as
it opened the card, so the callout was promoted first and the dialog covered it:
present, visible to every locator, and invisible to the user until something
re-promoted it - one Tab did, which is how this was spotted.

Modal already emits `open` after nldd-window's showModal(), so gate the in-card
steps on that. The e2e asserts it with elementFromPoint at the callout's centre;
toBeVisible() passes either way and would not have caught this.
robbertbos force-pushed tour-waits-for-async-banners from 247b575ccf
Some checks failed
CI / release-scripts (pull_request) Successful in 4s
security-scan / Filesystem scan (trivy fs) (pull_request) Failing after 5s
security-scan / SBOM (trivy) (pull_request) Successful in 10s
security-scan / JS SCA (npm audit) (pull_request) Successful in 16s
security-scan / Python SAST (bandit) (pull_request) Successful in 17s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 35s
test-build / build (frontend) (pull_request) Successful in 1m17s
test-build / build (backend) (pull_request) Successful in 1m21s
CI / pre-commit (pull_request) Successful in 1m23s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m25s
CI / backend-test (pull_request) Successful in 3m35s
CI / e2e (pull_request) Successful in 5m4s
to 259656060f
All checks were successful
CI / release-scripts (pull_request) Successful in 9s
security-scan / SBOM (trivy) (pull_request) Successful in 11s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 19s
security-scan / JS SCA (npm audit) (pull_request) Successful in 21s
security-scan / Python SAST (bandit) (pull_request) Successful in 22s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 45s
CI / pre-commit (pull_request) Successful in 1m16s
test-build / build (frontend) (pull_request) Successful in 1m13s
test-build / build (backend) (pull_request) Successful in 1m18s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m29s
CI / backend-test (pull_request) Successful in 3m37s
CI / e2e (pull_request) Successful in 4m30s
2026-08-13 08:08:21 +00:00
Compare
robbertbos changed title from Start the inbox tour only once the banners above it have settled to Keep the inbox tour on screen and in step with the user 2026-08-13 08:08:51 +00:00
robbertbos force-pushed tour-waits-for-async-banners from 259656060f
All checks were successful
CI / release-scripts (pull_request) Successful in 9s
security-scan / SBOM (trivy) (pull_request) Successful in 11s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 19s
security-scan / JS SCA (npm audit) (pull_request) Successful in 21s
security-scan / Python SAST (bandit) (pull_request) Successful in 22s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 45s
CI / pre-commit (pull_request) Successful in 1m16s
test-build / build (frontend) (pull_request) Successful in 1m13s
test-build / build (backend) (pull_request) Successful in 1m18s
test-build / build (pull_request) Successful in 0s
CI / frontend-test (pull_request) Successful in 1m29s
CI / backend-test (pull_request) Successful in 3m37s
CI / e2e (pull_request) Successful in 4m30s
to 73d44de58d
All checks were successful
CI / release-scripts (pull_request) Successful in 8s
security-scan / Python SCA (pip-audit) (pull_request) Successful in 56s
security-scan / Python SAST (bandit) (pull_request) Successful in 34s
security-scan / Filesystem scan (trivy fs) (pull_request) Successful in 19s
security-scan / JS SCA (npm audit) (pull_request) Successful in 28s
security-scan / SBOM (trivy) (pull_request) Successful in 16s
CI / pre-commit (pull_request) Successful in 2m24s
CI / frontend-test (pull_request) Successful in 2m20s
test-build / build (frontend) (pull_request) Successful in 1m32s
test-build / build (backend) (pull_request) Successful in 1m39s
test-build / build (pull_request) Successful in 0s
CI / backend-test (pull_request) Successful in 5m9s
CI / e2e (pull_request) Successful in 6m4s
2026-08-13 09:11:46 +00:00
Compare
robbertbos deleted branch tour-waits-for-async-banners 2026-08-13 09:23:23 +00:00
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
robbertbos/waggle!298
No description provided.