diff --git a/frontend/src/lib/components/WorkspaceTutorials.svelte b/frontend/src/lib/components/WorkspaceTutorials.svelte index 1eeacbe72f..f2f1bb63b6 100644 --- a/frontend/src/lib/components/WorkspaceTutorials.svelte +++ b/frontend/src/lib/components/WorkspaceTutorials.svelte @@ -1,6 +1,7 @@ + + { + return getTutorialSteps(driver) + }} +/> diff --git a/frontend/src/lib/components/tutorials/TroubleshootFlowTutorial.svelte b/frontend/src/lib/components/tutorials/TroubleshootFlowTutorial.svelte index 6fc53cf046..4fa9909e02 100644 --- a/frontend/src/lib/components/tutorials/TroubleshootFlowTutorial.svelte +++ b/frontend/src/lib/components/tutorials/TroubleshootFlowTutorial.svelte @@ -8,6 +8,7 @@ import { wait, type StateStore } from '$lib/utils' import { sendUserToast } from '$lib/toast' import { updateProgress } from '$lib/tutorialUtils' + import { DELAY_SHORT, DELAY_MEDIUM, DELAY_LONG, createFakeCursor } from './utils' interface Props { index: number @@ -30,11 +31,6 @@ 7: false }) - // Constants for delays - const DELAY_SHORT = 100 - const DELAY_MEDIUM = 300 - const DELAY_LONG = 500 - // Constants for cursor animation const CURSOR_START_OFFSET = -100 const CURSOR_CLICK_SCALE = 0.8 @@ -61,25 +57,13 @@ return true } - // Helper function to create and animate a fake cursor - async function createFakeCursor( + // Helper function to create and animate a fake cursor with start position + async function createFakeCursorWithStart( startElement: HTMLElement | null, endElement: HTMLElement, transitionDuration: number = 1.5 ): Promise { - const fakeCursor = document.createElement('div') - fakeCursor.style.cssText = ` - position: fixed; - width: 20px; - height: 20px; - border-radius: 50%; - background-color: rgba(59, 130, 246, 0.8); - border: 2px solid white; - pointer-events: none; - z-index: 10000; - transition: all ${transitionDuration}s ease-in-out; - ` - document.body.appendChild(fakeCursor) + const fakeCursor = createFakeCursor() const endRect = endElement.getBoundingClientRect() let startX: number, startY: number @@ -122,7 +106,7 @@ transitionDuration: number = 1.5, options?: { usePointerEvents?: boolean } ): Promise { - const fakeCursor = await createFakeCursor(null, element, transitionDuration) + const fakeCursor = await createFakeCursorWithStart(null, element, transitionDuration) await wait(DELAY_MEDIUM) // Animate click (shrink cursor briefly) diff --git a/frontend/src/lib/components/tutorials/utils.ts b/frontend/src/lib/components/tutorials/utils.ts index 1d07c1080c..083e9712dd 100644 --- a/frontend/src/lib/components/tutorials/utils.ts +++ b/frontend/src/lib/components/tutorials/utils.ts @@ -4,6 +4,17 @@ import { emptyApp } from '../apps/editor/appUtils' import type { App } from '../apps/types' import { findGridItem } from '../apps/editor/appUtilsCore' import { isRunnableByName } from '../apps/inputType' +import { wait } from '$lib/utils' + +// Tutorial animation delay constants +export const DELAY_SHORT = 100 +export const DELAY_MEDIUM = 300 +export const DELAY_LONG = 500 +export const DELAY_ANIMATION = 1500 +export const DELAY_ANIMATION_LONG = 2500 +export const DELAY_TYPING = 50 +export const DELAY_CODE_CHAR = 2 +export const DELAY_CODE_NEWLINE = 5 export function setInputBySelector(selector: string, value: string) { const input = document.querySelector(selector) as HTMLInputElement @@ -190,3 +201,128 @@ export function waitForElementLoading( attempts++ }, interval) } + +// Helper function to move cursor to element (for continuous cursor movement in tutorials) +export async function moveCursorToElement( + cursor: HTMLElement, + element: HTMLElement, + duration: number = DELAY_ANIMATION +): Promise { + const rect = element.getBoundingClientRect() + cursor.style.transition = `all ${duration / 1000}s ease-in-out` + cursor.style.left = `${rect.left + rect.width / 2}px` + cursor.style.top = `${rect.top + rect.height / 2}px` + await wait(duration) +} + +// Helper function to create a fake cursor element for tutorial animations +export function createFakeCursor(): HTMLElement { + const fakeCursor = document.createElement('div') + fakeCursor.style.cssText = ` + position: fixed; + width: 20px; + height: 20px; + border-radius: 50%; + background-color: rgba(59, 130, 246, 0.8); + border: 2px solid white; + pointer-events: none; + z-index: 10000; + transition: all 1.5s ease-in-out; + ` + document.body.appendChild(fakeCursor) + return fakeCursor +} + +// Constants for cursor animation +const CURSOR_START_OFFSET = -100 +const CURSOR_CLICK_SCALE = 0.8 + +// Helper function to create and animate a fake cursor with start position +export async function createFakeCursorWithStart( + startElement: HTMLElement | null, + endElement: HTMLElement, + transitionDuration: number = 1.5 +): Promise { + const fakeCursor = createFakeCursor() + + const endRect = endElement.getBoundingClientRect() + let startX: number, startY: number + + if (startElement) { + const startRect = startElement.getBoundingClientRect() + startX = startRect.left + startRect.width / 2 + startY = startRect.top + startRect.height / 2 + } else { + startX = endRect.left + CURSOR_START_OFFSET + startY = endRect.top + endRect.height / 2 + } + + fakeCursor.style.left = `${startX}px` + fakeCursor.style.top = `${startY}px` + + await wait(DELAY_SHORT) + + fakeCursor.style.left = `${endRect.left + endRect.width / 2}px` + fakeCursor.style.top = `${endRect.top + endRect.height / 2}px` + + await wait(transitionDuration * 1000) + + return fakeCursor +} + +// Helper function to animate a fake cursor click +export async function animateFakeCursorClick( + element: HTMLElement, + transitionDuration: number = 1.5, + options?: { usePointerEvents?: boolean; startElement?: HTMLElement | null } +): Promise { + const fakeCursor = await createFakeCursorWithStart( + options?.startElement ?? null, + element, + transitionDuration + ) + await wait(DELAY_MEDIUM) + + // Animate click (shrink cursor briefly) + fakeCursor.style.transform = `scale(${CURSOR_CLICK_SCALE})` + await wait(DELAY_SHORT) + fakeCursor.style.transform = 'scale(1)' + await wait(DELAY_SHORT) + + // Trigger pointer events if needed (flow graph uses pointer events instead of click) + if (options?.usePointerEvents) { + element.dispatchEvent(new PointerEvent('pointerdown', { bubbles: true })) + element.dispatchEvent(new PointerEvent('pointerup', { bubbles: true })) + } + + // Click the element + element.click() + await wait(DELAY_SHORT) + + // Remove fake cursor + fakeCursor.remove() +} + +// Helper function to animate cursor to element and click (for reusing a cursor across multiple clicks) +export async function animateCursorToElementAndClick( + cursor: HTMLElement, + element: HTMLElement, + startOffset: number = CURSOR_START_OFFSET +): Promise { + const rect = element.getBoundingClientRect() + + // Set initial position (off-screen to the left) + cursor.style.left = `${rect.left + startOffset}px` + cursor.style.top = `${rect.top + rect.height / 2}px` + await wait(DELAY_SHORT) + + // Animate to target position + cursor.style.left = `${rect.left + rect.width / 2}px` + cursor.style.top = `${rect.top + rect.height / 2}px` + await wait(DELAY_ANIMATION) + await wait(DELAY_MEDIUM) + + // Click on the element + element.click() + await wait(DELAY_SHORT) +} diff --git a/frontend/src/lib/components/tutorials/workspace/WorkspaceOnboardingOperatorTutorial.svelte b/frontend/src/lib/components/tutorials/workspace/WorkspaceOnboardingOperatorTutorial.svelte new file mode 100644 index 0000000000..5051719508 --- /dev/null +++ b/frontend/src/lib/components/tutorials/workspace/WorkspaceOnboardingOperatorTutorial.svelte @@ -0,0 +1,141 @@ + + + { + const steps: DriveStep[] = [ + { + popover: { + title: 'Welcome to Windmill! 🎉', + description: + "Let's take a quick tour! We'll show you the three main tools you can use: Scripts, Flows, and Apps.", + onNextClick: () => { + // Wait a bit to ensure the page is fully rendered before moving to next step + setTimeout(() => { + // Try to find the script tab button + const scriptsButton = document.querySelector('[data-value="script"]') as HTMLElement | null + + if (scriptsButton) { + driver.moveNext() + } else { + // If we can't find the button, just move to next step anyway + driver.moveNext() + } + }, 100) + } + } + }, + { + popover: { + title: 'Scripts - Run automated tasks', + description: + 'Script Example

Scripts are ready-to-use tasks that do things automatically for you.

You can run scripts whenever you need them - like generating a report, sending notifications, or processing data.

', + onNextClick: async () => { + // Move to the next step (Flows) + setTimeout(() => { + const flowsButton = document.querySelector('[data-value="flow"]') as HTMLElement | null + + if (flowsButton) { + driver.moveNext() + } else { + driver.moveNext() + } + }, 100) + } + }, + element: '[data-value="script"]' + }, + { + popover: { + title: 'Flows - Run step-by-step processes', + description: + 'Flow

Flows are processes that run multiple tasks in order, one after another.

You can start a flow and watch it complete each step automatically - perfect for tasks that have multiple stages.

', + onNextClick: async () => { + // Move to the next step (Apps) + setTimeout(() => { + const appsButton = document.querySelector('[data-value="app"]') as HTMLElement | null + + if (appsButton) { + driver.moveNext() + } else { + driver.moveNext() + } + }, 100) + } + }, + element: '[data-value="flow"]' + }, + { + popover: { + title: 'Apps - Use custom tools', + description: + 'App

Apps are easy-to-use tools with buttons, forms, and displays built just for your team.

You can open an app to work with your data, fill out forms, or trigger tasks - no technical knowledge needed!

', + onNextClick: async () => { + // Move to the next step (cursor animation) + driver.moveNext() + } + }, + element: '[data-value="app"]' + }, + { + popover: { + title: 'Finally, the Menu section', + description: 'Explore available tabs where you can access your history of runs, your scheduled scripts, your tutorials progress etc.

💡 Want to learn more? Access more tutorials from the Tutorials page in the main menu.

', + onNextClick: async () => { + // Find the target button and click it + const targetButton = document.querySelector('[role="menuitem"]') as HTMLElement | null + if (targetButton) { + targetButton.click() + } + + // Wait for menu to open + await wait(DELAY_MEDIUM) + + // Mark tutorial as complete + updateProgress(index) + driver.destroy() + + // Clean up URL parameter if present + if ($page.url.searchParams.has('tutorial')) { + goto(`${base}/`, { replaceState: true }) + } + } + }, + element: '[role="menuitem"]' + } + ] + + return steps + }} +/> diff --git a/frontend/src/lib/tutorialUtils.ts b/frontend/src/lib/tutorialUtils.ts index bd0ce09352..02222138e8 100644 --- a/frontend/src/lib/tutorialUtils.ts +++ b/frontend/src/lib/tutorialUtils.ts @@ -71,11 +71,6 @@ export async function skipAllTodos() { tutorialsToDo.set([]) skippedAll.set(true) - // Dismiss the tutorial banner when all tutorials are skipped - if (typeof window !== 'undefined') { - localStorage.setItem(TUTORIAL_BANNER_DISMISSED_KEY, 'true') - } - await UserService.updateTutorialProgress({ requestBody: { progress: bits, skipped_all: true } }) } diff --git a/frontend/src/lib/tutorials/config.ts b/frontend/src/lib/tutorials/config.ts index 3aa062e346..455c4c50e3 100644 --- a/frontend/src/lib/tutorials/config.ts +++ b/frontend/src/lib/tutorials/config.ts @@ -1,5 +1,5 @@ import type { ComponentType } from 'svelte' -import { Workflow, GraduationCap, Wrench, PlayCircle, Link2 } from 'lucide-svelte' +import { Workflow, GraduationCap, Wrench, PlayCircle, Link2, History } from 'lucide-svelte' import { base } from '$lib/base' import type { Role } from './roleUtils' @@ -43,7 +43,7 @@ export function getTutorialIndex(id: string): number { export const TUTORIALS_CONFIG: Record = { quickstart: { label: 'Quickstart', - roles: ['admin', 'developer'], + roles: ['admin', 'developer', 'operator'], progressBar: true, active: true, tutorials: [ @@ -88,7 +88,35 @@ export const TUTORIALS_CONFIG: Record = { comingSoon: false, roles: ['admin','developer'], order: 3 - } + }, + { + id: 'runs-tutorial', + icon: History, + title: 'Discover your monitoring dashboard', + description: 'Learn how to monitor, filter, and manage your script and flow executions.', + onClick: () => { + window.location.href = `${base}/runs?tutorial=runs-tutorial` + }, + index: 7, + active: true, + comingSoon: false, + roles: ['admin', 'developer','operator'], + order: 4 + }, + { + id: 'workspace-onboarding-operator', + icon: GraduationCap, + title: 'Workspace onboarding', + description: 'Discover the basics of Windmill with a quick tour of the workspace.', + onClick: () => { + window.location.href = `${base}/?tutorial=workspace-onboarding-operator` + }, + index: 6, + active: true, + comingSoon: false, + roles: ['operator'], + order: 1 + }, ] }, app_editor: { diff --git a/frontend/src/lib/tutorials/roleUtils.ts b/frontend/src/lib/tutorials/roleUtils.ts index 0372883784..a727fca8d3 100644 --- a/frontend/src/lib/tutorials/roleUtils.ts +++ b/frontend/src/lib/tutorials/roleUtils.ts @@ -53,12 +53,16 @@ export function hasRoleAccess( /** * Check if a preview role has access based on a roles array. * Used by admins to preview what other roles can see. - * This is a convenience wrapper around hasRoleAccess for preview mode. + * Uses exact role matching - only shows tutorials explicitly marked for the preview role. */ export function hasRoleAccessForPreview( previewRole: Role, roles?: Role[] ): boolean { - return hasRoleAccess(null, roles, previewRole) + // No roles specified = available to everyone + if (!roles || roles.length === 0) return true + + // Exact role match - tutorial must explicitly include the preview role + return roles.includes(previewRole) } diff --git a/frontend/src/routes/(root)/(logged)/+page.svelte b/frontend/src/routes/(root)/(logged)/+page.svelte index c668fc4374..687f5d156d 100644 --- a/frontend/src/routes/(root)/(logged)/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/+page.svelte @@ -110,6 +110,11 @@ setTimeout(() => { workspaceTutorials?.runTutorialById('workspace-onboarding') }, 500) + } else if (tutorialParam === 'workspace-onboarding-operator') { + // Small delay to ensure page is fully loaded + setTimeout(() => { + workspaceTutorials?.runTutorialById('workspace-onboarding-operator') + }, 500) } else if (!$ignoredTutorials.includes(8) && $tutorialsToDo.includes(8)) { // Check if user hasn't completed or ignored the workspace onboarding tutorial // Small delay to ensure page is fully loaded diff --git a/frontend/src/routes/(root)/(logged)/runs/[...path]/+page.svelte b/frontend/src/routes/(root)/(logged)/runs/[...path]/+page.svelte index 8d6c5dd9fe..85c1d8367e 100644 --- a/frontend/src/routes/(root)/(logged)/runs/[...path]/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/runs/[...path]/+page.svelte @@ -1,13 +1,31 @@ - {#key perPage} {/key} + + diff --git a/frontend/src/routes/(root)/(logged)/tutorials/+page.svelte b/frontend/src/routes/(root)/(logged)/tutorials/+page.svelte index 95ccbc9cc1..93ebdd4b20 100644 --- a/frontend/src/routes/(root)/(logged)/tutorials/+page.svelte +++ b/frontend/src/routes/(root)/(logged)/tutorials/+page.svelte @@ -54,7 +54,9 @@ // This derived value only recalculates when userStore or selectedPreviewRole changes const accessCheckContext = $derived.by(() => { const user = $userStore - const usePreview = user?.is_admin && selectedPreviewRole !== userEffectiveRole + // Always use preview mode for admins to show role-specific tutorials + // This ensures admins only see tutorials for the selected role + const usePreview = user?.is_admin return { user, usePreview, previewRole: selectedPreviewRole } }) diff --git a/frontend/static/script-tutorial-operator.png b/frontend/static/script-tutorial-operator.png new file mode 100644 index 0000000000..d64480473c Binary files /dev/null and b/frontend/static/script-tutorial-operator.png differ diff --git a/frontend/tutorial-system-guide.mdc b/frontend/tutorial-system-guide.mdc new file mode 100644 index 0000000000..47d71c2799 --- /dev/null +++ b/frontend/tutorial-system-guide.mdc @@ -0,0 +1,598 @@ +# Windmill Tutorial System Guide + +This guide documents the complete tutorial infrastructure in Windmill's frontend, enabling developers to create new interactive tutorials without re-exploring the codebase. + +## Table of Contents + +1. [Overview](#overview) +2. [Architecture](#architecture) +3. [File Structure](#file-structure) +4. [Creating a New Tutorial](#creating-a-new-tutorial) +5. [Key Components & APIs](#key-components--apis) +6. [Progress Tracking System](#progress-tracking-system) +7. [Role-Based Access](#role-based-access) +8. [Testing & Debugging](#testing--debugging) + +--- + +## Overview + +The Windmill tutorial system provides interactive, step-by-step guides for users using the `driver.js` library. Tutorials can: + +- Highlight specific UI elements with overlay popovers +- Guide users through workflows with navigation controls +- Track completion progress in the database +- Filter tutorials by user role (admin, developer, operator) +- Support multiple tutorial contexts (workspace, flow editor, app editor) + +**Core Technology:** [Driver.js](https://driverjs.com/) - A lightweight JavaScript library for creating product tours + +--- + +## Architecture + +### High-Level Flow + +``` +Tutorial Config (config.ts) + ↓ +Tutorial Registration (component creation) + ↓ +Tutorial Router (WorkspaceTutorials.svelte, etc.) + ↓ +URL Parameter Detection (+page.svelte) + ↓ +Tutorial Component (driver.js overlay) + ↓ +Progress Tracking (tutorialUtils.ts → backend) +``` + +### Component Hierarchy + +``` +TutorialRouter (manages multiple tutorials) + └── TutorialWrapper (wraps individual tutorials) + └── Tutorial (core driver.js engine) + ├── TutorialControls (prev/next buttons) + ├── SkipTutorials (skip options) + └── TutorialInner (loads driver.js CSS) +``` + +### State Management + +- **Global Stores** (`stores.ts`): + - `tutorialsToDo`: Array of incomplete tutorial indexes + - `skippedAll`: Boolean flag for skipped tutorials + - `isCurrentlyInTutorial`: Boolean tracking active tutorial state + +- **Progress Tracking** (`tutorialUtils.ts`): + - Uses 64-bit bitmask system (each bit = one tutorial) + - Syncs with backend `tutorial_progress` table + - Backend table: `tutorial_progress(email, progress bit(64))` + +--- + +## File Structure + +``` +frontend/src/lib/ +├── tutorials/ +│ ├── config.ts # Central tutorial registry +│ └── roleUtils.ts # Role-based access logic +│ +├── tutorialUtils.ts # Progress tracking utilities +├── stores.ts # Global stores (tutorialsToDo, etc.) +│ +└── components/ + ├── WorkspaceTutorials.svelte # Workspace tutorial container + ├── FlowTutorials.svelte # Flow editor tutorials container + ├── AppTutorials.svelte # App editor tutorials container + ├── RunPageTutorials.svelte # Run page tutorials container + │ + ├── tutorials/ + │ ├── Tutorial.svelte # Core tutorial engine (driver.js) + │ ├── TutorialRouter.svelte # Multi-tutorial manager + │ ├── TutorialWrapper.svelte # Instance wrapper + │ ├── TutorialInner.svelte # Loads driver.js CSS + │ ├── TutorialControls.svelte # Navigation UI + │ ├── SkipTutorials.svelte # Skip options + │ ├── ignoredTutorials.ts # Local storage for ignored tutorials + │ │ + │ ├── workspace/ + │ │ ├── WorkspaceOnboardingTutorial.svelte + │ │ └── WorkspaceOnboardingOperatorTutorial.svelte + │ │ + │ ├── app/ + │ │ ├── BackgroundRunnablesTutorial.svelte + │ │ ├── ConnectionTutorial.svelte + │ │ └── ExpressionEvaluationTutorial.svelte + │ │ + │ └── flow/ + │ ├── FlowBuilderLiveTutorial.svelte + │ └── TroubleshootFlowTutorial.svelte + │ + └── home/ + ├── TutorialButton.svelte # Tutorial card UI + └── TutorialBanner.svelte # Homepage banner +``` + +--- + +## Creating a New Tutorial + +### Step 1: Register Tutorial in Config + +**File:** `frontend/src/lib/tutorials/config.ts` + +```typescript +export const TUTORIALS_CONFIG: Record = { + quickstart: { + label: 'Quickstart', + roles: ['admin', 'developer', 'operator'], + progressBar: true, + active: true, + tutorials: [ + { + id: 'my-new-tutorial', // Unique identifier + icon: GraduationCap, // Lucide icon component + title: 'My New Tutorial', + description: 'Learn something new', + onClick: () => { + window.location.href = `${base}/?tutorial=my-new-tutorial` + }, + index: 7, // Next available index (1-64) + active: true, + comingSoon: false, + roles: ['developer', 'admin'], // Who can access + order: 7 + } + ] + } +} +``` + +**Important:** +- Choose a unique `index` (1-64) not used by other tutorials +- The `id` must match the tutorial parameter in the URL +- Indexes are used for bitmask progress tracking + +### Step 2: Create Tutorial Component + +**File:** `frontend/src/lib/components/tutorials/workspace/MyNewTutorial.svelte` + +```svelte + + + +``` + +### Step 3: Register in Tutorial Router + +**File:** `frontend/src/lib/components/WorkspaceTutorials.svelte` (or appropriate container) + +```svelte + + + + + + +``` + +### Step 4: Add URL Parameter Handling + +**File:** `frontend/src/routes/(root)/(logged)/+page.svelte` (or appropriate page) + +```svelte + + + +``` + +### Step 5: Test Your Tutorial + +1. Login as a user with the appropriate role +2. Navigate to the tutorials page +3. Click your tutorial card +4. Verify URL changes to `/?tutorial=my-new-tutorial` +5. Verify tutorial starts automatically +6. Step through all steps +7. Verify completion marks tutorial as done +8. Check database: `SELECT * FROM tutorial_progress WHERE email = 'your@email.com'` + +--- + +## Key Components & APIs + +### Tutorial.svelte + +**Core tutorial engine that wraps driver.js** + +**Props:** +- `index: number` - Tutorial index for progress tracking (1-64) +- `getSteps: (driver) => DriveStep[]` - Function returning tutorial steps + +**Exports:** +- `runTutorial(options?: any)` - Start the tutorial + +**Features:** +- Auto-completes tutorial when last step is finished +- Renders custom controls and skip options +- Calls `updateProgress(index)` on completion + +### TutorialRouter.svelte + +**Manages multiple tutorial instances** + +**Usage:** +```svelte + + + + +``` + +**Exports:** +- `runTutorialById(id: string, options?: any)` - Start tutorial by ID + +**Features:** +- Maintains Map of tutorial instances +- Routes calls to correct tutorial component +- Handles tutorial not found errors + +### DriveStep Interface + +**TypeScript interface for tutorial steps** + +```typescript +interface DriveStep { + element?: string // CSS selector to highlight + popover?: { + title: string + description: string // Supports HTML + onNextClick?: (element, step, context) => void + onPrevClick?: (element, step, context) => void + } +} +``` + +**Tips:** +- Omit `element` for non-highlighted steps (like welcome/completion) +- Use HTML in `description` for images: `` +- Use callbacks for custom navigation logic + +--- + +## Progress Tracking System + +### Bitmask System + +Tutorials use a 64-bit bitmask where each bit represents one tutorial's completion status: + +``` +Bit 0: Tutorial with index 0 (unused, reserve) +Bit 1: workspace-onboarding +Bit 2: flow-live-tutorial +Bit 3: troubleshoot-flow +Bit 4: backgroundrunnables +Bit 5: connection +Bit 6: workspace-onboarding-operator +... +Bit 63: Maximum possible tutorial +``` + +### Key Functions (tutorialUtils.ts) + +```typescript +// Mark tutorial as complete +await updateProgress(tutorialIndex: number) + +// Sync progress from backend +await syncTutorialsTodos() + +// Skip all tutorials +await skipAllTodos() + +// Reset all progress +await resetAllTodos() + +// Skip specific tutorials +await skipTutorialsByIndexes(indexes: number[]) + +// Complete specific tutorial +await completeTutorialByIndex(index: number) +``` + +### Backend Integration + +**Table:** `tutorial_progress` +```sql +CREATE TABLE tutorial_progress ( + email VARCHAR PRIMARY KEY, + progress BIT(64) +); +``` + +**API Endpoint:** `POST /api/users/tutorial_progress` +```typescript +// Request body +{ + "index": 7, // Tutorial index to mark complete +} +``` + +--- + +## Role-Based Access + +### Available Roles + +```typescript +type Role = 'admin' | 'developer' | 'operator' +``` + +### Role Hierarchy + +- **Admin**: Full access, can see all tutorials +- **Developer**: Standard developer tutorials +- **Operator**: Limited to operator-specific tutorials + +### Key Functions (roleUtils.ts) + +```typescript +// Get current user's role +const role = getUserEffectiveRole(user) + +// Check if user can access tutorial +const canAccess = hasRoleAccess(userRole, tutorialRoles) +``` + +### Setting Role Requirements + +In `config.ts`: + +```typescript +{ + id: 'operator-only-tutorial', + roles: ['operator'], // Only operators see this + // ... +} + +{ + id: 'admin-dev-tutorial', + roles: ['admin', 'developer'], // Admins and developers see this + // ... +} + +{ + id: 'everyone-tutorial', + roles: ['admin', 'developer', 'operator'], // Everyone sees this + // ... +} +``` + +--- + +## Testing & Debugging + +### Testing Checklist + +- [ ] Tutorial appears in correct tab/category +- [ ] Tutorial only visible to correct roles +- [ ] Clicking tutorial navigates to correct URL with tutorial parameter +- [ ] Tutorial auto-starts on page load with parameter +- [ ] All steps highlight correct elements +- [ ] Navigation controls work (prev/next) +- [ ] Skip options work correctly +- [ ] Completion marks tutorial as done in database +- [ ] Banner updates to reflect completion +- [ ] Tutorial doesn't auto-start after completion + +### Common Issues + +**Tutorial doesn't auto-start:** +- Check URL parameter matches tutorial ID in config +- Verify `onMount()` logic in page component +- Ensure tutorial component is registered in router + +**Element not highlighting:** +- Verify CSS selector is correct +- Check if element exists when tutorial runs +- Try using more specific selectors or IDs + +**Progress not saving:** +- Check tutorial index is unique and correctly passed +- Verify `updateProgress()` is called on final step +- Check network tab for API call to `/api/users/tutorial_progress` +- Inspect database `tutorial_progress` table + +**Wrong users see tutorial:** +- Verify `roles` array in config +- Check `getUserEffectiveRole()` returns correct role +- Ensure role filtering logic in tutorial list component + +### Debugging Tools + +**Browser Console:** +```javascript +// Check current tutorials to do +console.log($tutorialsToDo) + +// Check if tutorial is skipped +console.log($skippedAll) + +// Get user role +import { getUserEffectiveRole } from '$lib/tutorials/roleUtils' +console.log(getUserEffectiveRole($workspaceStore?.operator, $userStore)) +``` + +**Database Queries:** +```sql +-- Check user's tutorial progress +SELECT email, progress::text FROM tutorial_progress WHERE email = 'user@example.com'; + +-- Reset user's progress (testing) +UPDATE tutorial_progress SET progress = B'0' WHERE email = 'user@example.com'; + +-- See all tutorials and their completion +SELECT + email, + (progress & (1::bit(64) << 1))::int AS workspace_onboarding, + (progress & (1::bit(64) << 2))::int AS flow_live_tutorial, + (progress & (1::bit(64) << 3))::int AS troubleshoot_flow +FROM tutorial_progress; +``` + +--- + +## Best Practices + +### Tutorial Design + +1. **Keep It Short**: 4-7 steps is ideal +2. **Clear Objectives**: State what users will learn upfront +3. **Highlight Key Elements**: Focus on essential features +4. **Use Images**: Visual aids help comprehension +5. **End with Encouragement**: Congratulate users on completion + +### Technical Best Practices + +1. **Unique Indexes**: Always use unique index numbers (1-64) +2. **Stable Selectors**: Use IDs or specific classes for element highlighting +3. **Error Handling**: Wrap `updateProgress()` in try-catch +4. **Role Testing**: Test with all relevant user roles +5. **Mobile Friendly**: Ensure tutorials work on different screen sizes + +### Code Organization + +1. **Group by Context**: Workspace, flow, app tutorials in separate folders +2. **Consistent Naming**: `[Feature]Tutorial.svelte` convention +3. **Reusable Steps**: Extract common step patterns to utilities +4. **Document Complex Logic**: Add comments for non-obvious step behaviors + +--- + +## Quick Reference + +### Creating a New Tutorial (Checklist) + +- [ ] Step 1: Add to `config.ts` with unique ID and index +- [ ] Step 2: Create component in appropriate folder +- [ ] Step 3: Register in tutorial router (WorkspaceTutorials, etc.) +- [ ] Step 4: Add URL parameter handling in page component +- [ ] Step 5: Test with appropriate user role +- [ ] Step 6: Verify progress tracking in database + +### File Paths (Quick Copy) + +``` +# Config +frontend/src/lib/tutorials/config.ts + +# Tutorial Containers +frontend/src/lib/components/WorkspaceTutorials.svelte +frontend/src/lib/components/FlowTutorials.svelte +frontend/src/lib/components/AppTutorials.svelte + +# Tutorial Components +frontend/src/lib/components/tutorials/Tutorial.svelte +frontend/src/lib/components/tutorials/TutorialRouter.svelte +frontend/src/lib/components/tutorials/workspace/[YourTutorial].svelte + +# Page Integration +frontend/src/routes/(root)/(logged)/+page.svelte + +# Utilities +frontend/src/lib/tutorialUtils.ts +frontend/src/lib/tutorials/roleUtils.ts +``` + +--- + +## Additional Resources + +- **Driver.js Documentation**: https://driverjs.com/docs/ +- **Svelte Tutorial System Examples**: See existing tutorials in `frontend/src/lib/components/tutorials/` +- **Database Schema**: See `backend/summarized_schema.txt` for `tutorial_progress` table details