Skip to content

Unify Expert Mode with a beginner/expert onboarding experience#818

Merged
bdraco merged 39 commits into
mainfrom
onboarding-experience-levels
Jun 15, 2026
Merged

Unify Expert Mode with a beginner/expert onboarding experience#818
bdraco merged 39 commits into
mainfrom
onboarding-experience-levels

Conversation

@bdraco

@bdraco bdraco commented Jun 13, 2026

Copy link
Copy Markdown
Member

What does this implement/fix?

Unifies the dashboard's Expert Mode with the onboarding experience level into a single experience_level preference (beginner or expert). expert is Expert Mode: it unlocks the editor diff view, the device-navigator search, and the YAML content search. There is no separate expert_mode preference; expertModeContext is derived from experience_level === expert, so every consumer keys off the level.

Onboarding is a short stepped wizard (use case, experience, then Wi-Fi). The experience step offers Beginner or Expert. A remote-compute-only choice hides every device-creation entry point.

Settings → Appearance carries the Expert Mode toggle (its "what this unlocks" list collapses behind a chevron so the page doesn't grow) and, below it, the remote-compute toggle. There is no separate Experience section; both are editable any time.

Wi-Fi onboarding is unchanged for installs without Wi-Fi configured. If secrets.yaml has no Wi-Fi credentials we still prompt for them, since a device created without them generates an unresolvable !secret wifi_ssid. Existing installs are defaulted to the expert experience but are not acknowledged, so a missing-Wi-Fi install still auto-opens the standalone Wi-Fi dialog (not the experience wizard). "Maybe later" re-asks on the next login; only saving credentials or the explicit "I don't use Wi-Fi" decline stops it, exactly as before.

Preferences ride the subscribe_events initial_state snapshot (the same channel that seeds devices), so device creation gates on that snapshot rather than a separate config/get_preferences round-trip; the snapshot is always present, so the gate can't flash creation UI on a remote-compute install.

Companion backend PR: esphome/device-builder#1445

Related issue or feature (if applicable):

Types of changes

  • Bugfix (non-breaking change which fixes an issue) — bugfix
  • New feature (non-breaking change which adds functionality) — new-feature
  • Enhancement to an existing feature — enhancement
  • Breaking change (fix or feature that would cause existing functionality to not work as expected) — breaking-change
  • Refactor (no behaviour change) — refactor
  • Documentation only — docs
  • Maintenance / chore — maintenance
  • CI / workflow change — ci
  • Dependencies bump — dependencies

Checklist

  • The code change is tested and works locally.
  • npm run lint passes.
  • npm run test passes.
  • Tests have been added to verify that the new code works (where applicable).

Screenshots

Note: a few captures below predate the Expert-Mode unification. The experience step is now Beginner / Expert, and the Expert Mode toggle plus the remote-compute toggle now live in Settings, Appearance (the feature list behind a chevron) rather than a separate Experience section. Refreshed captures to follow.

First-run onboarding wizard (desktop / pip, not Home Assistant)

The wizard asks the use case, then the experience level, then Wi-Fi.

01-wizard-use-case 02-wizard-experience 03-wizard-wifi

Home Assistant add-on flow

No use-case question; the wizard opens at the experience step, then Wi-Fi.

04-wizard-ha-experience

Existing install

Defaulted to the YAML experience on startup, so the wizard never pops.

05-existing-install-no-wizard

Settings, Experience

The experience level and the remote-compute toggle are editable any time.
06-settings-appearance-collapsed
06b-settings-appearance-expanded

Remote-compute-only

Every device-creation entry point is hidden (no Add-device card, FAB, table Create, Adopt, or discovery). Future PR to add new remote dashboard state/status (out of scope for this PR).

07-remote-compute-dashboard

Change Wi-Fi credentials (kebab menu)

Standalone Wi-Fi reconfiguration still flows through its own dialog.

08-wifi-change-credentials

Editor first-open by experience

Beginner and UI open on the navigator; YAML opens the split view with the diff tools ready.

09-editor-beginner 11-editor-expert

Replaces the single-step Wi-Fi onboarding auto-pop with a short
stepped wizard and adds a persistent experience level the dashboard
tailors itself to:

- A stepped first-run wizard (use-case -> experience -> Wi-Fi). The
  use-case step appears on non-HA installs; choosing remote-compute
  drops the Wi-Fi step. The standalone Wi-Fi rotation dialog stays for
  the kebab entry; both share a wifi-fields helper.
- An experience level (beginner / ui / yaml) and a remote-compute-only
  toggle, both editable any time in a new Settings -> Experience
  section. Experience is the sole driver of the YAML diff button
  (beginners off, UI / YAML on) and the editor's first-open layout
  (YAML users land in the split view), so the manual Settings -> Editor
  toggle and the command-palette quick-toggle are removed.
- Remote-compute-only hides every device-creation entry point
  (New device, FAB, table Create, Adopt, serial wizard).

Coordinated with the device-builder backend PR that adds the
experience_level / remote_compute_only preferences and the
environment-aware onboarding steps.
@esphbot

esphbot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Previous review — superseded by a newer review below.

@esphbot esphbot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking issues found — see the review comment above.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a first-run onboarding wizard that captures a user “experience level” and optionally enables a remote-compute-only mode, then uses these preferences to adapt the dashboard/editor UI and hide device-creation entry points when appropriate.

Changes:

  • Introduces a stepped onboarding wizard (use-case → experience → Wi‑Fi, with conditional steps) and shared Wi‑Fi input rendering utilities.
  • Adds experience_level + remote_compute_only preferences, contexts, and Settings → Experience section to edit them post-onboarding.
  • Updates dashboard/editor behavior based on experience level and remote-compute-only mode (layout seeding, hiding creation affordances).

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
test/util/onboarding-gate.test.ts Adds unit tests for Wi‑Fi-step-specific pending detection.
src/util/onboarding-gate.ts Adds isWifiSetupPending() to scope Wi‑Fi pending logic to the Wi‑Fi step only.
src/translations/en.json Adds new wizard + experience/settings copy; removes old editor/diff-toggle copy.
src/pages/device.ts Seeds first-open editor layout from experience_level when no local layout is saved.
src/pages/dashboard.ts Consumes remoteComputeOnlyContext and blocks serial auto-wizard in remote-compute-only mode.
src/context/index.ts Re-exports new experience/remote-compute contexts.
src/context/contexts.ts Defines experienceLevelContext and remoteComputeOnlyContext.
src/components/settings-dialog/types.ts Adds new Settings section id for “experience”; removes legacy “editor” section.
src/components/settings-dialog/experience-section.ts New Settings UI for experience level selection + remote-compute-only toggle.
src/components/settings-dialog/editor-section.ts Removes legacy YAML-diff-button toggle section.
src/components/settings-dialog.ts Wires new Experience section and icons; removes editor section wiring.
src/components/onboarding/wifi-fields.ts New shared Wi‑Fi SSID/password rendering + validation helper.
src/components/onboarding/onboarding-wizard-styles.ts New shared styles for the onboarding wizard dialog.
src/components/onboarding/onboarding-wizard-dialog.ts New multi-step onboarding wizard dialog (conditional flow + Wi‑Fi save + acknowledge).
src/components/onboarding/choice-card.ts New reusable selectable “choice card” renderer for wizard/settings.
src/components/onboarding/choice-card-styles.ts New shared styling for choice cards across wizard and settings.
src/components/onboarding-wifi-dialog.ts Refactors to use shared renderWifiFields() and isWifiPasswordTooShort().
src/components/dashboard/render-toolbar.ts Hides “add device” card/FAB when remote-compute-only is enabled.
src/components/dashboard/render-content.ts Hides discovery adopt section and table create action when remote-compute-only is enabled.
src/components/command-palette.ts Removes YAML-diff toggle integration from the command palette.
src/components/command-palette-actions.ts Removes editor/diff-toggle command actions.
src/components/app-shell/settings-actions.ts Adds optimistic persistence for experience level + remote-compute-only with revert+toast on failure.
src/components/app-shell/data-load.ts Loads new preferences + adjusts onboarding pending logic and use-case presence detection.
src/components/app-shell.ts Provides new contexts, mounts wizard dialog, and wires new settings events.
src/api/types/system.ts Adds ExperienceLevel, new preferences fields, and new onboarding step ids.
Comments suppressed due to low confidence (1)

test/util/onboarding-gate.test.ts:41

  • stateWith() is typed as ReturnType<typeof wifi>[], but the new tests pass experience(...) steps too. This makes the test file fail TypeScript type-checking because OnboardingStepId.EXPERIENCE_LEVEL isn’t assignable to OnboardingStepId.WIFI_CREDENTIALS. Type steps as OnboardingState["steps"] (or OnboardingStep[]) so the helper matches the real API shape.
const stateWith = (
  steps: ReturnType<typeof wifi>[],
  current_version = 1,
  completed_version = 0
): OnboardingState => ({
  current_version,
  completed_version,
  steps,
});

Comment thread src/components/onboarding/onboarding-wizard-dialog.ts
Comment thread src/components/onboarding/onboarding-wizard-dialog.ts Outdated
Comment thread src/components/onboarding/onboarding-wizard-dialog.ts Outdated
Comment thread src/components/onboarding/onboarding-wizard-dialog.ts Outdated
Comment thread src/components/settings-dialog/experience-section.ts Outdated
- Add an in-flight guard so a reconnect mid-write can't reload the
  pre-write snapshot over an optimistic experience / remote-compute
  value (mirrors the existing _remoteBuildSetInFlight pattern).
- Log preference-load failures instead of swallowing them; experience
  and remote-compute have no localStorage backing, so a silent failure
  could leave device creation visible on an install that wanted it hidden.
- The wizard now persists its picks (awaited) before acknowledging, so a
  failed write can't mark onboarding done with experience / remote-compute
  unset and never re-pop. Drops the fire-and-forget navigation dispatches
  (a dismissed wizard no longer half-commits) and the app shell refreshes
  prefs on the acknowledged event.
- Label the choice radiogroups and hide the decorative step dots from the
  accessibility tree.
- Back renderWifiFields with a shared style module so .field / .error
  can't drift between the wizard and the rotation dialog.
@bdraco

bdraco commented Jun 13, 2026

Copy link
Copy Markdown
Member Author

Addressed the Kōan and Copilot reviews in 7b1f067.

  • Reconnect race: experience and remote-compute writes now set an in-flight flag and loadThemePreference skips while it is set, so a mid-write reconnect can't revert the optimistic value.
  • Dead dispatcher: confirmed gone; editor-section.ts is deleted and the command-palette quick-toggle is removed, so grep -rn set-yaml-diff-button src/ returns zero dispatch sites.
  • Silent load failure: loadThemePreference now logs the error rather than swallowing it.
  • Wizard acknowledge-before-persist: the wizard now awaits its preference writes before acknowledging and shows an inline error on failure, so it can't be marked done with experience or remote-compute unset. It also no longer half-commits when dismissed, and the app shell refreshes prefs on the acknowledged event.
  • radiogroup names: all three choice radiogroups now carry an aria-label.
  • Step dots: marked aria-hidden so the decorative spans stay out of the accessibility tree.
  • Shared styles: renderWifiFields is backed by a shared wifiFieldsStyles module so .field / .error can't drift between the wizard and the rotation dialog.

Verified the full wizard flow end to end on a fresh install; it persists experience_level=yaml, yaml_diff_button=true, remote_compute_only=false, acknowledges, and writes the wifi secret.

The wizard and the Settings experience section each rebuilt the same
experience-option array in render() and duplicated the
level-to-yaml-diff-button rule. Move both to src/util/experience.ts
(EXPERIENCE_OPTIONS, yamlDiffForExperience) so the option list, icons,
and the diff-button mapping have one source of truth and aren't
re-allocated per render.
@esphbot

esphbot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

@bdraco: Addressed the Kōan and Copilot reviews in 7b1f067.

Verified all seven at 7b1f067. Persist-before-ack correct: _acknowledgeAndClose writes prefs, returns on failure, never acknowledges. Dead dispatcher gone: set-yaml-diff-button zero hits; yamlDiffButtonContext correctly kept (device-editor still consumes it). Reconnect gate, radiogroup labels, aria-hidden dots, shared wifiFieldsStyles all present. Remaining items are suggestions only: radiogroup arrow-key nav, wizard branch-logic tests, two pre-existing orphan keys.

@esphbot

esphbot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Previous review — superseded by a newer review below.

@esphbot esphbot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

- Remove two pre-existing unused keys (layout.yaml_diff_button,
  settings.editor) flagged in review; both had zero call sites.
- Reword the loadThemePreference catch comment to match behaviour: the
  last loaded values are kept (not reset), and the failure is logged
  rather than toasted because it runs on every reconnect.
@bdraco

bdraco commented Jun 13, 2026

Copy link
Copy Markdown
Member Author

Addressed the cleanup items in 0f1d41b: pruned the two pre-existing orphan keys (layout.yaml_diff_button, settings.editor; both had zero call sites) and reworded the loadThemePreference catch to match behaviour, since the last loaded values are kept (not reset) and the failure is logged rather than toasted because it runs on every reconnect.

Left as follow-ups: the radiogroup roving-tabindex / arrow-key model (the cards are keyboard-operable today via Tab plus Enter/Space and the groups are labelled, so it is usable; full roving focus would thread focus management through the shared render helper and is cleaner as its own change), and extracting _screens for branch tests.

@esphbot

esphbot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

@bdraco: Addressed the cleanup items in 0f1d41b: pruned the two pre-existing orphan keys (`layout.yaml_diff_b...

Reword matches behavior. Catch keeps last values, resets nothing — sticky after first success. Disagree with prior HIGH. One residual edge: first-ever getPreferences failure leaves remote_compute_only default false, briefly rendering creation UI. Low impact, retries on reconnect. Tabindex deferral reasonable — cards operable via Tab plus Enter/Space, groups labelled. Separate note: _prefsSetInFlight now guards three write paths; consider a counter so overlapping experience/remote-compute writes don't clear the gate early.

@esphbot

esphbot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Previous review — superseded by a newer review below.

@esphbot esphbot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking issues found — see the review comment above.

bdraco added 2 commits June 13, 2026 15:02
Split the onboarding auto-show so a fresh install (experience not yet
chosen) gets the full wizard, while an existing install that already has
an experience but is missing Wi-Fi gets only the standalone Wi-Fi dialog,
which carries the "I don't use Wi-Fi" decline. Adds isExperienceChosen
to the gate and a _onboardingShowWifi flag the app shell opens.
_prefsSetInFlight was a single boolean guarding two write paths
(experience plus yaml-diff, and remote-compute). Two overlapping writes
let the first settle and clear the gate while the second was still
pending, so a reconnect could reload the pre-write snapshot over it.
Replace it with a counter incremented per write and decremented in
finally; loadThemePreference now skips while it is greater than zero, so
every in-flight write must settle before the gate reopens.
@bdraco

bdraco commented Jun 13, 2026

Copy link
Copy Markdown
Member Author

Fixed in feb0b9d. Replaced the single _prefsSetInFlight boolean with a counter (_prefsWritesInFlight): each write increments on entry and decrements in finally, and loadThemePreference skips while it is greater than zero, so two overlapping writes both have to settle before the reconnect gate reopens.

On the pref-load suggestion: agreed with your reassessment; the catch keeps the last loaded values (it never resets them) and logs rather than toasting, since loadThemePreference runs on every reconnect.

@esphbot

esphbot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Previous review — superseded by a newer review below.

@esphbot esphbot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Blocking issues found — see the review comment above.

bdraco added 2 commits June 13, 2026 15:18
Covers the previously-untested frontend logic for this feature:
- onSetExperienceLevel / onSetRemoteComputeOnly: optimistic set, the
  experience-to-yaml-diff seed, and revert + toast on rejection.
- The prefs-write in-flight counter: two overlapping writes keep the
  gate above zero until both settle.
- loadOnboardingState routing: fresh installs open the wizard, existing
  installs missing wifi open the wifi dialog, configured installs open
  neither, and a session dismissal is respected.
- loadThemePreference skips while a write is in flight and otherwise
  loads experience / remote-compute.
- yamlDiffForExperience and EXPERIENCE_OPTIONS.
The wizard's _screens getter inlined the branch logic (use-case only
non-HA, wifi dropped for remote-compute), which couldn't be unit-tested
without the component. Move it to a pure wizardScreens() helper and pin
all four environment/use-case combinations.
@esphbot

esphbot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

@bdraco: Fixed in feb0b9d. Replaced the single _prefsSetInFlight boolean with a counter (`_prefsWritesInFli...

Counter approach correct. Overlapping experience + remote-compute writes now both settle before reconnect gate reopens. One gap remains: onSetTheme never increments _prefsWritesInFlight and stays .catch(() => {}). So a reconnect mid theme-write can still revert the optimistic theme. Low impact — theme cosmetic, localStorage-backed. Flagged as suggestion, not blocker.

Skip called markOnboardingAcknowledged, which bumped
onboarding_completed_version and permanently silenced the Wi-Fi prompt
even though secrets.yaml still held the placeholder values. Skip now
persists the experience pick but does not acknowledge, and session-
dismisses, so the standalone Wi-Fi dialog (with its "I don't use Wi-Fi"
decline) re-asks on the next login while Wi-Fi is unconfigured. Only
saving credentials or the explicit decline stops the prompt.
@esphbot

esphbot commented Jun 13, 2026

Copy link
Copy Markdown
Contributor

Previous review — superseded by a newer review below.

bdraco added 2 commits June 14, 2026 15:41
…-levels

# Conflicts:
#	src/translations/en.json
…-levels

# Conflicts:
#	src/translations/en.json
Copilot AI review requested due to automatic review settings June 14, 2026 21:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 36 out of 36 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings June 15, 2026 14:51

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 33 changed files in this pull request and generated 2 comments.

Comment thread src/components/app-shell/data-load.ts
Comment thread src/components/settings-dialog/experience-section.ts Outdated
…-levels

# Conflicts:
#	src/api/types/event-subscription.ts
#	src/components/app-shell.ts
#	src/components/app-shell/data-load.ts
#	src/components/app-shell/events.ts
#	test/components/app-shell/events-initial-state-prefs.test.ts
Copilot AI review requested due to automatic review settings June 15, 2026 15:37

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 32 out of 32 changed files in this pull request and generated 4 comments.

Comment thread src/components/onboarding/onboarding-wizard-dialog.ts
Comment thread src/components/app-shell/settings-actions.ts Outdated
Comment thread src/components/settings-dialog/appearance-section.ts
Comment thread src/components/app-shell/data-load.ts Outdated
Copilot AI review requested due to automatic review settings June 15, 2026 15:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 33 out of 33 changed files in this pull request and generated 3 comments.

Comment thread src/components/settings-dialog/appearance-section.ts
Comment thread src/components/app-shell/data-load.ts Outdated
Comment thread src/components/settings-dialog.ts
@bdraco bdraco changed the title Add experience-level onboarding wizard and remote-compute mode Unify Expert Mode with a beginner/expert onboarding experience Jun 15, 2026
@esphbot

esphbot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

@bdraco: The one flagged item (onSetTheme's log-only catch) is intentional, as the review notes: theme is non...

Agree. Theme non-security-sensitive. applyTheme writes localStorage (app-shell.ts:374), so a failed server persist still survives reload and self-corrects on next successful load. Log-only matches repo convention — revert+toast is scoped to security-sensitive toggles. No change needed.

@esphbot

esphbot commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

PR Review — Unify Expert Mode with a beginner/expert onboarding experience

Solid, well-iterated PR. No blocking issues in the reviewed files; per repo policy a maintainer (Steven/Marcel) must merge, and the companion backend PR (esphome/device-builder#1445) must land in lockstep.

Specific strengths:

  • The _prefsWritesInFlight counter (not a boolean) correctly handles overlapping writes across the theme/experience/remote-compute paths — both must settle before a reconnect snapshot can reconcile, closing the reconnect-revert race cleanly.
  • Device creation fails closed: _prefsLoaded gates every creation entry point (Add-device card → nothing, FAB, table Create, discovery section, serial-connect toast) until the always-present initial_state snapshot lands, so a remote-compute install can't flash creation UI.
  • Deriving _expertMode from _experienceLevel in willUpdate makes the level the single source of truth and keeps the provided context reactive; no separate expert_mode preference to drift.
  • Correct lockstep handling: InitialStateEventData.preferences is required (no optional guard / version-skew shim), matching CLAUDE.md.
  • The isWifiSetupPending/isExperienceChosen split routes existing-but-missing-Wi-Fi installs to the standalone dialog and fresh installs to the full wizard — gating logic verified correct.

Notes (non-blocking):

  • onSetTheme log-only-on-failure is intentional and correct: theme is non-security-sensitive and applyTheme persists to localStorage (verified app-shell.ts:374), which re-applies on next load. The MEDIUM silent-failure flag is overstated for this control — CLAUDE.md scopes mandatory revert+toast to security-sensitive toggles.
  • Unverified (files omitted from diff / stale local mirror): the unified experience.ts/EXPERIENCE_OPTIONS, the onboarding wizard's await-before-acknowledge, a11y on the new radiogroups, and en.json key cleanup. Prior review rounds report these addressed; I could not independently confirm them here.


Checklist

  • No hardcoded secrets
  • Optimistic updates revert + toast on sensitive toggles
  • Error handling does not swallow errors silently
  • No backwards-compat shims (lockstep deployment)
  • Device-creation gating fails closed until prefs load
  • WS-boundary type/doc consistency (preferences required)
  • Removed localization keys deleted from en.json — unverified — file omitted from diff
  • ARIA/a11y on new interactive controls — unverified — file omitted from diff

Automated review by Kōan (Claude) HEAD=ab3bf0b 4 min 41s

@esphbot esphbot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No blocking issues found.

@stvncode stvncode left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lgtm 🚀

@bdraco bdraco merged commit 6b6a679 into main Jun 15, 2026
8 checks passed
@bdraco bdraco deleted the onboarding-experience-levels branch June 15, 2026 16:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

new-feature New feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug] Why “Show YAML diff button"

4 participants