mirror of
https://github.com/maziggy/bambuddy.git
synced 2026-10-03 20:56:18 +02:00
Closes the recurring "I added the printer but it isn't connecting" +
"where is X feature" cluster that drove ~1/3 of invalid-tagged issues.
New users now see a friendly welcome modal on first load and can take
a guided walkthrough that highlights the load-bearing UI surface
step-by-step.
Backend: two new nullable columns on users (onboarding_status
VARCHAR(64), onboarding_snoozed_until TIMESTAMP) added via inline
migration with a one-shot backfill that marks every existing user as
dismissed_at_migration -- pre-existing installs never see the welcome
modal. GET + PATCH /api/v1/users/me/onboarding round-trip the state;
OnboardingUpdate schema rejects dismissed_at_migration from the API
(migration-internal only), validates the tour_in_progress:<step_id>
form, and enforces snooze coherence (snoozed_until required only when
status is snoozed). _users_column_exists helper mirrors
_api_keys_column_exists and gates the backfill so restarts after
new-user signups do not clobber the welcome-eligible NULL state.
SQLite and Postgres both verified end-to-end.
Frontend architecture: OnboardingProvider wraps the app inside
AuthProvider; reads from the backend when auth is on, falls back to
localStorage (bambuddy.onboarding_status + onboarding_snoozed_until)
when auth is off so no-auth installs still get the welcome
experience. A loadFailed gate prevents the welcome modal from popping
over a backend outage -- we cannot distinguish "new user" from "GET
errored" so we stay silent. OnboardingFlow driver picks between the
Phase 0 welcome modal, the Phase 0.2 about modal, and the
step-by-step TourEngine based on persisted status. Mounted inside
BrowserRouter so the route guard's useLocation has its context (a
sibling-of-Router placement crashed at runtime).
Route guard: the overlay never renders on /setup, /login,
/spoolbuddy/*, /camera/*, /overlay/*, or while requiresSetup is
true -- fresh installs walk through the existing /setup flow
uninterrupted, and the SpoolBuddy kiosk / OBS overlay / camera-popout
windows never get a modal slapped over them.
Tour engine: 25 steps targeting existing data-tour anchors --
add-printer -> verify-connection -> printer-card sub-tour x5 (status
row, AMS row, camera, controls, customize menu) -> add-spool ->
bambu-cloud-sync -> sidebar overview x6 -> vp -> slicer-api ->
makerworld -> obico -> integrations -> notifications -> users ->
groups -> sso -> outro. Per-step route navigation via useNavigate,
anchor polling at 100ms intervals with a 3-second cap (pages need a
beat after navigation; never spin forever), box-shadow dimmed-
spotlight cutout that pointer-events-through so the user can still
interact, smart modal positioning (sidebar anchors to the right;
page anchors below or flipped above based on viewport room),
Back/Next/Skip with Escape as Skip, persistence on every Back/Next
so mid-tour reloads resume at the same step.
Conditional skip: each step exposes a skipIf(ctx) evaluated against
at least one printer exists; verify-connection + the entire card
sub-tour skip when no printer exists; makerworld skips when the user
lacks makerworld:view (the sidebar entry is permission-gated and the
anchor would not resolve); users / groups / sso skip when auth is
off. Pre-render gate means the user never sees a flash of a step
that is about to skip.
Phase 1.1 "Lock the front door" auth step explicitly removed from
the live tour -- /setup already prompts for the auth choice on fresh
installs, and users who deliberately chose no-auth must not be
nudged to enable it. The auth-card anchor stays for any future
privacy-checkup surface.
BB mascot: hero pose + 5 named poses (started / walk / almost /
allset / help) sliced from the character sheet via Pillow into
public/img/bb_*.webp. MascotIcon component takes a pose prop;
per-step pose mapping in tourSteps.ts. TourLauncher sits in the
sidebar footer as a BB icon that relaunches the tour from step 0
and consumes the [data-tour="help-icon"] selector.
WikiHelpIcon component lands the per-page question-mark icon on
Queue / Archives / Stats / Maintenance / Files / Projects /
Inventory -- opens the matching wiki.bambuddy.cool/{path}/ page in
a new tab (chose new-tab over iframe because the wiki sets
X-Frame-Options: DENY).
27 new data-tour anchors across PrintersPage / SettingsPage /
ProfilesPage / InventoryPage / Layout. i18n: 138 new keys under a
new onboarding.* namespace, real translations in every locale
(de/en/es/fr/it/ja/ko/pt-BR/tr/zh-CN/zh-TW) -- no
IDENTICAL_TO_EN_ALLOWED entries added.
Backend tests: 12 in TestOnboardingAPI cover the round-trips and the
validator branches. Frontend tests: 61 across 7 files -- anchor
presence backstop, Phase 0 modal interaction, tour engine + step
helpers, provider state machine (auth-on/off split, PATCH error
fall-through, localStorage round-trip), route guard for every
suppress path, WikiHelpIcon href/target/aria. Backend pytest
5723/5723; frontend vitest 2154/2154; ESLint clean; frontend build
clean; i18n parity clean at 5233 leaves x 11 locales.
Companion docs: docs/onboarding-tour-plan.md carries the design
(Phase 0-5 step inventory, anchor selector list, state model, asset
inventory, resolved decisions, Implementation Status section that
is authoritative for current shipped state).
React + TypeScript + Vite
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
Currently, two official plugins are available:
- @vitejs/plugin-react uses Babel (or oxc when used in rolldown-vite) for Fast Refresh
- @vitejs/plugin-react-swc uses SWC for Fast Refresh
React Compiler
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see this documentation.
Expanding the ESLint configuration
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Remove tseslint.configs.recommended and replace with this
tseslint.configs.recommendedTypeChecked,
// Alternatively, use this for stricter rules
tseslint.configs.strictTypeChecked,
// Optionally, add this for stylistic rules
tseslint.configs.stylisticTypeChecked,
// Other configs...
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])
You can also install eslint-plugin-react-x and eslint-plugin-react-dom for React-specific lint rules:
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
// Other configs...
// Enable lint rules for React
reactX.configs['recommended-typescript'],
// Enable lint rules for React DOM
reactDom.configs.recommended,
],
languageOptions: {
parserOptions: {
project: ['./tsconfig.node.json', './tsconfig.app.json'],
tsconfigRootDir: import.meta.dirname,
},
// other options...
},
},
])