Files
wizarr/WIZARD_ARCHITECTURE.md

14 KiB

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/<idx>          # Pre-invite phase steps
/pre-wizard/complete       # Pre-invite completion (redirects to join page)
/post-wizard/<idx>         # Post-invite phase steps
/wizard/complete           # Post-invite completion page
/wizard/<server>/<idx>     # Preview mode (admin/testing)
/wizard/combo/<idx>        # Multi-server invitations

Request Flow

  1. Initial Page Load

    • User visits /pre-wizard or /post-wizard/<idx>
    • 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/<code>)
    • Post-invite: Redirects to completion page (/wizard/complete)

State Management

Server → Client Communication

Flask sends custom headers on HTMX requests:

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:

<div id="wizard-wrapper"
     data-current-idx="0"
     data-max-idx="5"
     data-server-type="plex"
     data-phase="pre|post|preview"
     data-step-phase="pre|post"
     data-completion-url="/pre-wizard/complete"
     data-completion-label="Continue to Invite">

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:

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:

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:

@wizard_bp.route("/pre-wizard/<int:idx>")
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:

@wizard_bp.route("/post-wizard/<int:idx>")
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:

// 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:

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/<code>)
  • Marks pre-wizard as complete in session

Session Management:

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:

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:

@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