Documentation for Jetty

Interactive Tutorial: Your First Tunnel

Overview

This document describes the implementation details for the "Your First Tunnel" interactive tutorial feature. The tutorial guides new users through creating their first Jetty tunnel with step-by-step validation, UI highlights, and real-time progress tracking.

Purpose

The interactive tutorial system provides:

  • Guided onboarding for new users unfamiliar with the CLI workflow
  • Progressive disclosure of features and concepts
  • Real-time validation that users complete each step correctly
  • Contextual help and troubleshooting at each stage
  • Gamification through badges, points, and celebrations

Architecture

Components

The tutorial system consists of four main components:

  1. Tutorial Definition (your-first-tunnel.json) - Declarative JSON structure defining steps, validation, and UI behavior
  2. Tutorial Engine (Frontend) - React/Vue component that renders steps and manages state
  3. Validation API (Backend) - Laravel endpoints that verify step completion
  4. UI Highlighting (Frontend) - DOM manipulation to highlight specific elements

Data Flow

User Action → Frontend State Update → Backend Validation → Progress Update → Next Step

JSON Schema Reference

Root Structure

{
  "id": "string",              // Unique tutorial identifier
  "title": "string",           // Display title
  "description": "string",     // Short description for listings
  "category": "string",        // Category (onboarding, advanced, etc.)
  "duration": number,          // Estimated minutes
  "difficulty": "string",      // beginner|intermediate|advanced
  "tags": ["string"],          // Search/filter tags
  "prerequisites": ["string"], // Required prerequisites
  "learningObjectives": ["string"],
  "completionRewards": {...},
  "steps": [...],
  "summary": {...},
  "metadata": {...}
}

Step Structure

Each step in the steps array:

{
  "id": "string",              // Unique step ID
  "title": "string",           // Step title
  "description": "string",     // Short description
  "content": "string",         // Markdown content body
  "icon": "string",            // Icon name (heroicons, fontawesome, etc.)
  "codeExample": {...},        // Optional code snippet
  "validation": {...},         // How to verify completion
  "helpText": "string",        // Markdown help content
  "tips": ["string"],          // Quick tips array
  "troubleshooting": [...],    // Common issues and solutions
  "nextAction": "string",      // Clear call-to-action
  "nextButton": "string",      // Button text
  "skipAllowed": boolean,      // Can user skip this step?
  "celebration": {...}         // Animation/message on completion
}

Validation Types

Manual Validation

User manually confirms completion (no automatic check):

{
  "type": "manual",
  "message": "Ready to proceed"
}

API-Based Validation

Backend endpoint checks completion status:

{
  "type": "api_token_exists",
  "message": " Token created successfully!",
  "endpoint": "/api/tutorial/validate/token-exists",
  "pollInterval": 2000,    // Poll every 2 seconds
  "timeout": 300000        // 5 minute timeout
}

Conditional Validation

Multiple conditions must be met:

{
  "type": "multiple_requests",
  "message": " Multiple requests logged!",
  "endpoint": "/api/tutorial/validate/multiple-requests",
  "pollInterval": 3000,
  "timeout": 300000,
  "criteria": {
    "minRequests": 3
  }
}

UI Highlighting

Target DOM elements to draw attention:

{
  "uiHighlight": {
    "selector": "#create-token-button",  // CSS selector
    "position": "right",                 // tooltip position
    "offset": {
      "x": 10,
      "y": 0
    }
  }
}

Code Examples

Display copyable code snippets:

{
  "codeExample": {
    "language": "bash",
    "code": "jetty share http://localhost:8000",
    "copyable": true,
    "explanation": "This creates a public tunnel"
  }
}

Interactive Demos

Simulate terminal interactions:

{
  "interactiveDemo": {
    "type": "terminal-simulation",
    "steps": [
      {
        "input": "jetty auth:login",
        "output": "Enter your API token: "
      },
      {
        "input": "jetty_xxxxxxxxxxxx",
        "output": " Authentication successful!"
      }
    ]
  }
}

Frontend Implementation

Tutorial Engine Component

Create a React component to manage tutorial state:

// components/Tutorial/TutorialEngine.tsx

interface TutorialState {
  currentStepIndex: number;
  completedSteps: string[];
  skippedSteps: string[];
  validationInProgress: boolean;
  tutorialData: Tutorial;
}

const TutorialEngine: React.FC<{ tutorialId: string }> = ({ tutorialId }) => {
  const [state, setState] = useState<TutorialState>({
    currentStepIndex: 0,
    completedSteps: [],
    skippedSteps: [],
    validationInProgress: false,
    tutorialData: null
  });

  useEffect(() => {
    // Load tutorial JSON
    fetch(`/api/tutorials/${tutorialId}`)
      .then(res => res.json())
      .then(data => setState(prev => ({ ...prev, tutorialData: data })));
  }, [tutorialId]);

  const currentStep = state.tutorialData?.steps[state.currentStepIndex];

  return (
    <div className="tutorial-container">
      <TutorialHeader tutorial={state.tutorialData} />
      <ProgressBar 
        current={state.currentStepIndex} 
        total={state.tutorialData?.steps.length} 
      />
      <StepRenderer 
        step={currentStep}
        onComplete={handleStepComplete}
        onSkip={handleStepSkip}
      />
    </div>
  );
};

Step Renderer

Render individual step content:

// components/Tutorial/StepRenderer.tsx

const StepRenderer: React.FC<{ step: Step; onComplete: () => void }> = ({ 
  step, 
  onComplete 
}) => {
  const [validating, setValidating] = useState(false);

  const handleValidation = async () => {
    if (step.validation.type === 'manual') {
      onComplete();
      return;
    }

    setValidating(true);
    
    // Poll validation endpoint
    const pollValidation = setInterval(async () => {
      const response = await fetch(step.validation.endpoint);
      const result = await response.json();

      if (result.valid) {
        clearInterval(pollValidation);
        setValidating(false);
        showCelebration(step.celebration);
        onComplete();
      }
    }, step.validation.pollInterval);

    // Timeout
    setTimeout(() => {
      clearInterval(pollValidation);
      setValidating(false);
    }, step.validation.timeout);
  };

  return (
    <div className="step-content">
      <StepHeader icon={step.icon} title={step.title} />
      <MarkdownContent content={step.content} />
      
      {step.codeExample && (
        <CodeBlock 
          language={step.codeExample.language}
          code={step.codeExample.code}
          copyable={step.codeExample.copyable}
        />
      )}

      {step.tips && <TipsPanel tips={step.tips} />}
      {step.troubleshooting && <TroubleshootingPanel items={step.troubleshooting} />}

      <button 
        onClick={handleValidation}
        disabled={validating}
        className="next-button"
      >
        {validating ? 'Validating...' : step.nextButton}
      </button>
    </div>
  );
};

UI Highlighting System

Highlight specific UI elements during tutorial steps:

// components/Tutorial/UIHighlight.tsx

const UIHighlight: React.FC<{ highlight: UIHighlightConfig }> = ({ highlight }) => {
  const [targetRect, setTargetRect] = useState<DOMRect | null>(null);

  useEffect(() => {
    const element = document.querySelector(highlight.selector);
    if (element) {
      const rect = element.getBoundingClientRect();
      setTargetRect(rect);

      // Add spotlight overlay
      element.classList.add('tutorial-highlight');

      return () => {
        element.classList.remove('tutorial-highlight');
      };
    }
  }, [highlight.selector]);

  if (!targetRect) return null;

  // Position tooltip based on highlight.position
  const tooltipStyle = calculateTooltipPosition(targetRect, highlight.position, highlight.offset);

  return (
    <>
      {/* Dimmed overlay */}
      <div className="tutorial-overlay" />
      
      {/* Spotlight cutout */}
      <div 
        className="tutorial-spotlight"
        style={{
          left: targetRect.left,
          top: targetRect.top,
          width: targetRect.width,
          height: targetRect.height
        }}
      />

      {/* Tooltip */}
      <div className="tutorial-tooltip" style={tooltipStyle}>
        {/* Tooltip content */}
      </div>
    </>
  );
};

Celebration Animations

Show visual feedback on step completion:

// components/Tutorial/Celebrations.tsx

const showCelebration = (celebration: CelebrationConfig) => {
  switch (celebration.animation) {
    case 'confetti-small':
      confetti({ particleCount: 50, spread: 60 });
      break;
    case 'confetti-large':
      confetti({ particleCount: 200, spread: 90 });
      break;
    case 'check-bounce':
      animateCheckmark();
      break;
    case 'rocket':
      animateRocket();
      break;
  }

  toast.success(celebration.message, {
    duration: 3000,
    icon: 'check'
  });
};

Backend Implementation

Validation API Endpoints

Create Laravel routes for validation:

// routes/api.php

Route::middleware('auth:sanctum')->group(function () {
    Route::get('/tutorial/validate/token-exists', [TutorialValidationController::class, 'validateTokenExists']);
    Route::get('/tutorial/validate/cli-authenticated', [TutorialValidationController::class, 'validateCliAuthenticated']);
    Route::get('/tutorial/validate/tunnel-active', [TutorialValidationController::class, 'validateTunnelActive']);
    Route::get('/tutorial/validate/tunnel-request', [TutorialValidationController::class, 'validateTunnelRequest']);
    Route::get('/tutorial/validate/multiple-requests', [TutorialValidationController::class, 'validateMultipleRequests']);
    Route::get('/tutorial/validate/dashboard-viewed', [TutorialValidationController::class, 'validateDashboardViewed']);
    Route::get('/tutorial/validate/tunnel-stopped', [TutorialValidationController::class, 'validateTunnelStopped']);
});

Validation Controller

Implement validation logic:

// app/Http/Controllers/TutorialValidationController.php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;

class TutorialValidationController extends Controller
{
    public function validateTokenExists(Request $request): JsonResponse
    {
        $user = $request->user();
        
        // Check if user has created at least one API token
        $hasToken = $user->tokens()->count() > 0;

        return response()->json([
            'valid' => $hasToken,
            'message' => $hasToken ? 'Token found' : 'No token created yet'
        ]);
    }

    public function validateCliAuthenticated(Request $request): JsonResponse
    {
        $user = $request->user();

        // Check for recent CLI authentication (token used within last 5 minutes)
        $hasRecentCliAuth = $user->tokens()
            ->where('last_used_at', '>', now()->subMinutes(5))
            ->exists();

        return response()->json([
            'valid' => $hasRecentCliAuth,
            'message' => $hasRecentCliAuth ? 'CLI authenticated' : 'Waiting for CLI authentication'
        ]);
    }

    public function validateTunnelActive(Request $request): JsonResponse
    {
        $user = $request->user();

        // Check for active tunnel
        $activeTunnel = $user->tunnels()
            ->where('status', 'active')
            ->latest()
            ->first();

        return response()->json([
            'valid' => $activeTunnel !== null,
            'message' => $activeTunnel ? 'Tunnel is active' : 'No active tunnel',
            'data' => $activeTunnel ? [
                'id' => $activeTunnel->id,
                'subdomain' => $activeTunnel->subdomain,
                'url' => $activeTunnel->public_url
            ] : null
        ]);
    }

    public function validateTunnelRequest(Request $request): JsonResponse
    {
        $user = $request->user();

        // Check if any tunnel has received at least one request
        $hasRequest = $user->tunnels()
            ->whereHas('requests')
            ->exists();

        return response()->json([
            'valid' => $hasRequest,
            'message' => $hasRequest ? 'Request received' : 'No requests yet'
        ]);
    }

    public function validateMultipleRequests(Request $request): JsonResponse
    {
        $user = $request->user();
        $minRequests = $request->query('minRequests', 3);

        // Get most recent tunnel
        $tunnel = $user->tunnels()->latest()->first();

        if (!$tunnel) {
            return response()->json([
                'valid' => false,
                'message' => 'No tunnel found'
            ]);
        }

        $requestCount = $tunnel->requests()->count();

        return response()->json([
            'valid' => $requestCount >= $minRequests,
            'message' => "Received {$requestCount} of {$minRequests} requests",
            'data' => [
                'current' => $requestCount,
                'required' => $minRequests
            ]
        ]);
    }

    public function validateDashboardViewed(Request $request): JsonResponse
    {
        // This could check session data or analytics events
        // For now, we'll use a simple flag stored in the session
        $viewed = session()->has('tutorial_dashboard_viewed');

        return response()->json([
            'valid' => $viewed,
            'message' => $viewed ? 'Dashboard viewed' : 'Dashboard not viewed yet'
        ]);
    }

    public function validateTunnelStopped(Request $request): JsonResponse
    {
        $user = $request->user();

        // Check that the most recent tunnel is now inactive
        $latestTunnel = $user->tunnels()->latest()->first();

        $isStopped = $latestTunnel && $latestTunnel->status !== 'active';

        return response()->json([
            'valid' => $isStopped,
            'message' => $isStopped ? 'Tunnel stopped' : 'Tunnel still active'
        ]);
    }
}

Tutorial Progress Tracking

Track user progress through tutorials:

// app/Models/TutorialProgress.php

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class TutorialProgress extends Model
{
    protected $fillable = [
        'user_id',
        'tutorial_id',
        'current_step_id',
        'completed_steps',
        'skipped_steps',
        'completed_at',
        'started_at'
    ];

    protected $casts = [
        'completed_steps' => 'array',
        'skipped_steps' => 'array',
        'started_at' => 'datetime',
        'completed_at' => 'datetime'
    ];

    public function user()
    {
        return $this->belongsTo(User::class);
    }

    public function isCompleted(): bool
    {
        return $this->completed_at !== null;
    }

    public function percentComplete(): float
    {
        $tutorial = $this->getTutorialDefinition();
        $totalSteps = count($tutorial['steps']);
        $completedCount = count($this->completed_steps);

        return ($completedCount / $totalSteps) * 100;
    }

    private function getTutorialDefinition(): array
    {
        $path = resource_path("tutorials/{$this->tutorial_id}.json");
        return json_decode(file_get_contents($path), true);
    }
}

Integration Points

Dashboard Integration

Add tutorial trigger to dashboard:

// resources/views/dashboard.blade.php

@if(!$user->hasCompletedTutorial('your-first-tunnel'))
    <div class="tutorial-prompt">
        <h3>New to Jetty?</h3>
        <p>Take a 5-minute tutorial to create your first tunnel</p>
        <button onclick="startTutorial('your-first-tunnel')">
            Start Tutorial
        </button>
    </div>
@endif

CLI Coordination

The CLI should set appropriate flags when users perform actions:

// jetty-client/src/Commands/ShareCommand.php

protected function execute(InputInterface $input, OutputInterface $output): int
{
    // ... existing share logic ...

    // Update last_used_at for CLI authentication validation
    $this->updateTokenUsage();

    // Mark tunnel as active
    $this->apiClient->post("/tunnels/{$tunnelId}/activate");

    // ... rest of implementation ...
}

Styling

CSS Structure

/* Tutorial overlay and spotlight */
.tutorial-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0, 0, 0, 0.7);
  z-index: 9998;
  backdrop-filter: blur(2px);
}

.tutorial-spotlight {
  position: fixed;
  background: white;
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.7);
  border-radius: 8px;
  z-index: 9999;
  pointer-events: none;
  transition: all 0.3s ease;
}

.tutorial-highlight {
  position: relative;
  z-index: 10000;
  box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.5);
  border-radius: 8px;
}

/* Tutorial tooltip */
.tutorial-tooltip {
  position: fixed;
  background: white;
  border-radius: 12px;
  padding: 1.5rem;
  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
  max-width: 400px;
  z-index: 10001;
}

/* Step content */
.step-content {
  padding: 2rem;
  max-width: 800px;
  margin: 0 auto;
}

.code-block {
  background: #1e1e1e;
  border-radius: 8px;
  padding: 1rem;
  margin: 1rem 0;
  position: relative;
}

.copy-button {
  position: absolute;
  top: 0.5rem;
  right: 0.5rem;
  opacity: 0.7;
  transition: opacity 0.2s;
}

.copy-button:hover {
  opacity: 1;
}

/* Progress bar */
.progress-bar {
  height: 4px;
  background: #e5e7eb;
  border-radius: 2px;
  overflow: hidden;
}

.progress-fill {
  height: 100%;
  background: linear-gradient(90deg, #3b82f6, #8b5cf6);
  transition: width 0.3s ease;
}

Testing

Frontend Tests

// __tests__/TutorialEngine.test.tsx

describe('TutorialEngine', () => {
  it('loads tutorial definition', async () => {
    const { getByText } = render(<TutorialEngine tutorialId="your-first-tunnel" />);
    await waitFor(() => {
      expect(getByText('Share Your First Tunnel')).toBeInTheDocument();
    });
  });

  it('progresses through steps', async () => {
    const { getByText, getByRole } = render(<TutorialEngine tutorialId="your-first-tunnel" />);
    
    // Complete first step
    const nextButton = getByRole('button', { name: /I've created my token/i });
    fireEvent.click(nextButton);

    await waitFor(() => {
      expect(getByText('Open Your Terminal')).toBeInTheDocument();
    });
  });

  it('validates steps correctly', async () => {
    // Mock validation endpoint
    fetchMock.get('/api/tutorial/validate/token-exists', {
      valid: true,
      message: 'Token found'
    });

    const { getByRole, getByText } = render(<TutorialEngine tutorialId="your-first-tunnel" />);
    
    const nextButton = getByRole('button', { name: /I've created my token/i });
    fireEvent.click(nextButton);

    await waitFor(() => {
      expect(getByText(/Token created successfully/i)).toBeInTheDocument();
    });
  });
});

Backend Tests

// tests/Feature/TutorialValidationTest.php

class TutorialValidationTest extends TestCase
{
    public function test_validates_token_exists()
    {
        $user = User::factory()->create();
        $user->createToken('test-token');

        $response = $this->actingAs($user)
            ->getJson('/api/tutorial/validate/token-exists');

        $response->assertJson([
            'valid' => true
        ]);
    }

    public function test_validates_active_tunnel()
    {
        $user = User::factory()->create();
        $tunnel = Tunnel::factory()->create([
            'user_id' => $user->id,
            'status' => 'active'
        ]);

        $response = $this->actingAs($user)
            ->getJson('/api/tutorial/validate/tunnel-active');

        $response->assertJson([
            'valid' => true,
            'data' => [
                'id' => $tunnel->id
            ]
        ]);
    }

    public function test_validates_multiple_requests()
    {
        $user = User::factory()->create();
        $tunnel = Tunnel::factory()->create(['user_id' => $user->id]);
        
        // Create 5 requests
        TunnelRequest::factory()->count(5)->create([
            'tunnel_id' => $tunnel->id
        ]);

        $response = $this->actingAs($user)
            ->getJson('/api/tutorial/validate/multiple-requests?minRequests=3');

        $response->assertJson([
            'valid' => true,
            'data' => [
                'current' => 5,
                'required' => 3
            ]
        ]);
    }
}

Best Practices

Content Writing

  1. Be conversational - Write like you're guiding a friend
  2. Use clear CTAs - Every step should have one obvious next action
  3. Provide context - Explain why each step matters
  4. Anticipate confusion - Add troubleshooting for common issues
  5. Celebrate progress - Positive reinforcement keeps users engaged

Validation Design

  1. Poll frequently - 2-3 second intervals for responsive feedback
  2. Set reasonable timeouts - 5 minutes is usually sufficient
  3. Provide partial progress - Show "2 of 3 requests received" style feedback
  4. Handle edge cases - What if user has old tunnels/tokens?
  5. Graceful degradation - Allow manual override if validation fails

UI/UX Guidelines

  1. Don't block the UI - Users should be able to close tutorial anytime
  2. Persist progress - Save state so users can resume later
  3. Mobile-friendly - Tutorial should work on all screen sizes
  4. Accessible - Proper ARIA labels, keyboard navigation
  5. Performance - Don't poll validation endpoints too aggressively

Extensibility

Adding New Tutorials

  1. Create JSON definition in docs/tutorials/{tutorial-id}.json
  2. Implement any custom validation endpoints needed
  3. Add tutorial to the catalog in docs/tutorials/catalog.json
  4. Create accompanying markdown documentation
  5. Add tests for validation logic

Custom Validation Types

Register custom validators:

// lib/tutorialValidators.ts

const customValidators = {
  'webhook_configured': async (step: Step) => {
    const response = await fetch('/api/validate/webhook');
    return response.json();
  },
  'custom_domain_verified': async (step: Step) => {
    const response = await fetch('/api/validate/domain');
    return response.json();
  }
};

export const validateStep = async (step: Step) => {
  const validator = customValidators[step.validation.type];
  if (validator) {
    return await validator(step);
  }
  
  // Fall back to API endpoint
  return fetch(step.validation.endpoint).then(r => r.json());
};

Performance Considerations

Caching

Cache tutorial definitions in Redis:

// app/Services/TutorialService.php

public function getTutorial(string $tutorialId): array
{
    return Cache::remember("tutorial:{$tutorialId}", 3600, function () use ($tutorialId) {
        $path = resource_path("tutorials/{$tutorialId}.json");
        return json_decode(file_get_contents($path), true);
    });
}

Validation Optimization

Batch validation checks where possible:

public function validateMultipleSteps(Request $request): JsonResponse
{
    $checks = $request->input('checks', []);
    $results = [];

    foreach ($checks as $check) {
        $results[$check] = $this->runValidation($check);
    }

    return response()->json($results);
}

Future Enhancements

Potential improvements to the tutorial system:

  • A/B testing - Test different tutorial flows to optimize completion rates
  • Analytics - Track where users drop off to improve problematic steps
  • Branching paths - Different flows based on user's framework (Laravel vs Node.js)
  • Video integration - Embed short screencasts for complex steps
  • Interactive CLI simulation - Browser-based terminal emulator for practice
  • Progress sync - Sync tutorial progress across devices
  • Achievements system - Unlock badges and rewards for completing tutorials
  • Social sharing - Share completion with team or on social media

Support

For questions or issues implementing tutorials:

  • Open an issue on GitHub
  • Check existing tutorials for patterns
  • Review test cases for validation examples
  • Consult the UI component library for standard elements

Send feedback

Found an issue or have a suggestion? Let us know.