# Wizard Architecture Documentation > **Version:** 3.0 - Two-Phase Wizard System > **Last Updated:** 2025-10-04 ## Overview The Wizarr wizard is a mobile-first, app-like onboarding system with: - **Two-phase flow** (pre-invite and post-invite steps) - **Fixed UI chrome** (progress bar + navigation buttons) - **Smooth content transitions** (slide animations) - **Swipe gesture support** - **Zero UI duplication** (HTMX partial swaps) - **Phase-aware routing** (separate endpoints for each phase) --- ## Two-Phase Wizard System ### Phase Overview The wizard operates in two distinct phases: 1. **Pre-Invite Phase** (`/pre-wizard`) - Shown **before** user accepts invitation - No authentication required (invite code in session) - Validates invite code on each request - Completes with redirect to join page - Use cases: Terms of service, requirements, welcome messages 2. **Post-Invite Phase** (`/post-wizard`) - Shown **after** user accepts invitation - Requires authentication or `wizard_access` session - Completes with redirect to completion page - Use cases: App setup, library selection, feature tours ### User Flow ``` User clicks invite link ↓ [Invite code stored in session] ↓ Pre-Invite Steps (if configured) ↓ Join Page (accept invitation) ↓ [Account created] ↓ Post-Invite Steps (if configured) ↓ Completion Page ``` --- ## Architecture ### File Structure ``` app/templates/wizard/ ├── frame.html # Initial page load wrapper ├── steps.html # UI chrome (progress + buttons) └── _content.html # Content-only partial (HTMX swaps) app/blueprints/wizard/ └── routes.py # Flask routes with phase-aware template selection app/services/ └── invite_code_manager.py # Session management for invite codes ``` ### Routing Structure ``` /wizard/ # Legacy entry point (redirects based on context) /pre-wizard/ # Pre-invite phase steps /pre-wizard/complete # Pre-invite completion (redirects to join page) /post-wizard/ # Post-invite phase steps /wizard/complete # Post-invite completion page /wizard// # Preview mode (admin/testing) /wizard/combo/ # Multi-server invitations ``` ### Request Flow 1. **Initial Page Load** - User visits `/pre-wizard` or `/post-wizard/` - Flask returns `frame.html` - `frame.html` includes `steps.html` - `steps.html` includes `_content.html` - **Result:** Full page with UI chrome + initial content 2. **HTMX Navigation** - User clicks button or swipes - HTMX sends request with `HX-Request` header - Flask detects header and returns `_content.html` ONLY - HTMX swaps `#wizard-content` element - JavaScript updates progress bars and button states - **Result:** Content slides in, UI chrome stays static 3. **Phase Completion** - **Pre-invite:** Redirects to join page (`/invite/`) - **Post-invite:** Redirects to completion page (`/wizard/complete`) --- ## State Management ### Server → Client Communication Flask sends custom headers on HTMX requests: ```python resp.headers['X-Wizard-Idx'] = str(idx) resp.headers['X-Require-Interaction'] = 'true' | 'false' resp.headers['X-Wizard-Step-Phase'] = 'pre' | 'post' | '' ``` ### Client State Storage The `#wizard-wrapper` element stores: ```html
``` **Phase Attributes:** - `data-phase`: Overall wizard phase context (pre, post, or preview) - `data-step-phase`: Current step's specific phase (for mixed-phase flows) - `data-completion-url`: URL to redirect when completing this phase - `data-completion-label`: Button text for completion action ### JavaScript Controller `WizardController` handles all UI updates: ```javascript WizardController.updateUI(xhr) → updateProgress(idx, maxIdx) // Animate progress bars → updateButtons(idx, maxIdx, ...) // Update URLs & visibility (phase-aware) → updatePhaseIndicator(phase) // Update phase badge ``` --- ## Mobile Experience ### Layout Structure ``` ┌─────────────────────────────┐ │ Fixed Progress Bar (top) │ ← position: fixed, z-index: 40 ├─────────────────────────────┤ │ │ │ Scrollable Content Area │ ← overflow-y: auto, flex: 1 │ (only this scrolls) │ │ │ ├─────────────────────────────┤ │ Fixed Nav Buttons (bottom) │ ← position: fixed, z-index: 40 │ [←] [→] │ (circular, gradient) └─────────────────────────────┘ ``` ### CSS Classes - `.wizard-container` - Fixed viewport height container - `.wizard-progress-mobile` - Fixed progress bar - `.wizard-content-mobile` - Scrollable content area - `.wizard-nav-mobile` - Fixed button container - `.wizard-btn-mobile` - Circular gradient buttons ### Swipe Gestures - **Swipe Left** → Navigate to next step - **Swipe Right** → Navigate to previous step - **Threshold:** 75px - Respects disabled states and boundaries --- ## Template Logic ### Flask Route Pattern All wizard routes follow this pattern: ```python def _serve_wizard(server: str, idx: int, steps: list, phase: str, *, completion_url: str | None = None, completion_label: str | None = None, current_step_phase: str | None = None): # ... build HTML content ... # Smart template selection if not request.headers.get("HX-Request"): page = "wizard/frame.html" # Initial load else: page = "wizard/_content.html" # HTMX swap response = render_template( page, body_html=html, idx=idx, max_idx=len(steps) - 1, server_type=server, phase=phase, # NEW: Phase context step_phase=display_phase, # NEW: Current step phase completion_url=completion_url, # NEW: Completion redirect completion_label=completion_label # NEW: Completion button text ) # Add headers for HTMX requests if request.headers.get("HX-Request"): resp = make_response(response) resp.headers['X-Wizard-Idx'] = str(idx) resp.headers['X-Require-Interaction'] = 'true' | 'false' resp.headers['X-Wizard-Step-Phase'] = display_phase or '' # NEW return resp return response ``` ### Phase-Specific Routes **Pre-Wizard Route:** ```python @wizard_bp.route("/pre-wizard/") def pre_wizard(idx: int = 0): # Validate invite code invite_code = InviteCodeManager.get_invite_code() # ... validation logic ... # Get pre-invite steps steps = _steps(server_type, cfg, category="pre_invite") # Render with pre-phase context return _serve_wizard( server_type, idx, steps, "pre", completion_url=url_for("wizard.pre_wizard_complete"), completion_label=_("Continue to Invite") ) ``` **Post-Wizard Route:** ```python @wizard_bp.route("/post-wizard/") def post_wizard(idx: int = 0): # Check authentication if not current_user.is_authenticated and not session.get("wizard_access"): return redirect(url_for("auth.login")) # Get post-invite steps steps = _steps(server_type, cfg, category="post_invite") # Render with post-phase context return _serve_wizard(server_type, idx, steps, "post") ``` ### Button URL Updates Buttons dynamically update their `hx-get` attribute based on phase: ```javascript // Phase-aware URL generation if (phase === 'pre') { btn.setAttribute('hx-get', `/pre-wizard/${targetIdx}`); } else if (phase === 'post') { btn.setAttribute('hx-get', `/post-wizard/${targetIdx}`); } else { btn.setAttribute('hx-get', `/wizard/${serverType}/${targetIdx}`); } htmx.process(wrapper); // Reinitialize HTMX ``` ### Step Filtering by Category The `_steps()` function filters steps by category: ```python def _steps(server: str, cfg: dict, category: str = "post_invite"): """Return ordered wizard steps for server and category. Args: server: Server type (plex, jellyfin, etc.) cfg: Configuration dictionary category: 'pre_invite' or 'post_invite' (default: 'post_invite') Returns: List of wizard steps filtered by category """ db_rows = ( WizardStep.query .filter_by(server_type=server, category=category) .order_by(WizardStep.position) .all() ) # ... fallback to legacy markdown files (post_invite only) ... ``` --- ## Key Design Decisions ### ✅ DO - Only swap `#wizard-content` - Update progress/buttons via JavaScript - Use `_content.html` for HTMX responses - Send state via custom headers (including phase) - Validate all indices before updates - Filter steps by category (`pre_invite` vs `post_invite`) - Validate invite codes on each pre-wizard request - Use phase-specific completion URLs ### ❌ DON'T - Never swap `#wizard-wrapper` (causes duplication) - Never return `steps.html` for HTMX requests - Never modify progress bars via HTMX swaps - Never skip header validation - Never mix pre-invite and post-invite steps in the same phase - Never allow pre-wizard access without valid invite code - Never hardcode server types in fallback logic --- ## Debugging ### Console Logging The wizard logs key events: ``` WizardController initialized ✅ Wizard update: { newIdx: 1, maxIdx: 5, serverType: 'plex', phase: 'pre', requireInteraction: false } Button wizard-prev-btn: URL=/pre-wizard/0, visible=true Button wizard-next-btn: URL=/pre-wizard/2, visible=true Phase indicator updated: pre ``` ### Common Issues | Issue | Cause | Solution | |-------|-------|----------| | Duplicate progress bars | Swapping wrapper instead of content | Check Flask returns `_content.html` for HTMX | | Button URLs don't update | HTMX not reprocessed | Ensure `htmx.process(wrapper)` is called | | Content doesn't scroll | Container height wrong | Verify `.wizard-container` has `height: 100vh` | | Buttons behind blur | Z-index issue | Ensure `.wizard-btn-mobile` has `z-index: 50` | | Wrong phase steps shown | Category filter incorrect | Verify `_steps()` receives correct `category` parameter | | Pre-wizard accessible without invite | Session validation missing | Check `InviteCodeManager.validate_invite_code()` is called | | Completion redirects to wrong page | Phase-specific completion URL not set | Verify `completion_url` parameter in `_serve_wizard()` | --- ## Phase-Specific Behavior ### Pre-Invite Phase **Access Control:** - Requires valid invite code in session - No authentication required - Validates invite code on each request - Redirects to home page if invite code is invalid/expired **Completion:** - Final step shows "Continue to Invite" button - Redirects to `/pre-wizard/complete` - Completion endpoint redirects to join page (`/invite/`) - Marks pre-wizard as complete in session **Session Management:** ```python InviteCodeManager.get_invite_code() # Retrieve invite code InviteCodeManager.validate_invite_code(code) # Validate invite code InviteCodeManager.mark_pre_wizard_complete() # Mark phase complete ``` ### Post-Invite Phase **Access Control:** - Requires authentication OR `wizard_access` session - User must have accepted invitation - No invite code validation needed **Completion:** - Final step shows "Next" button (or custom label) - Redirects to `/wizard/complete` - Completion endpoint clears all session data - Shows success message and redirects to app **Session Cleanup:** ```python InviteCodeManager.clear_invite_data() # Clear invite-related data session.pop("wizard_access", None) # Clear wizard access flag session.pop("wizard_server_order", None) session.pop("wizard_bundle_id", None) ``` ### Backward Compatibility The `/wizard/` endpoint provides backward compatibility: ```python @wizard_bp.route("/") def start(): """Redirect to appropriate wizard based on context.""" if current_user.is_authenticated or session.get("wizard_access"): return redirect(url_for("wizard.post_wizard")) invite_code = InviteCodeManager.get_invite_code() if invite_code: return redirect(url_for("wizard.pre_wizard")) return redirect(url_for("public.root")) ``` --- ## Future Enhancements - [ ] Keyboard navigation (arrow keys) - [ ] Accessibility improvements (ARIA live regions) - [ ] Progress persistence (localStorage) - [ ] Prefetch next step for instant navigation - [ ] Step validation hooks - [ ] Custom step transitions - [ ] Phase-specific analytics tracking - [ ] Conditional phase skipping based on invitation settings --- ## Testing Checklist ### Core Functionality - [ ] Initial page load shows all UI chrome - [ ] Navigation updates only content area - [ ] Progress bar animates smoothly - [ ] Previous button hidden on first step - [ ] Next button hidden on last step - [ ] Swipe gestures work correctly - [ ] Required interaction blocks navigation - [ ] Mobile layout fits single viewport - [ ] Desktop layout stays centered - [ ] Dark mode works correctly ### Phase-Specific Tests - [ ] Pre-wizard validates invite code on each request - [ ] Pre-wizard redirects to home if invite code invalid - [ ] Pre-wizard completion redirects to join page - [ ] Post-wizard requires authentication - [ ] Post-wizard completion clears session data - [ ] Post-wizard completion shows success message - [ ] Legacy `/wizard/` route redirects correctly - [ ] Multi-server invitations show correct phase steps - [ ] Phase badges display correctly - [ ] Completion buttons show correct labels