15 KiB
Cutscene Onboarding System
Table of Contents
- Overview
- Steps
- Stages
- Prerequisites
- Groups
- Adding Content
- Overlay Engine
- Store & Persistence
- Onboarding Page
- Mobile
Overview
Cutscene is an interactive onboarding system that guides users through Profilarr by cutting holes in the scene. A dark overlay covers the application and spotlight cutouts reveal the elements the user needs to interact with. Users click real buttons, navigate real pages, and make real changes while the overlay controls what's visible and clickable.
The name comes from two things: cutting holes in the scene, and like a gaming cutscene, the app takes over to guide you through a narrative before handing control back.
When a cutscene starts, the overlay finds the current step's target element via
data-onboarding attributes in the DOM, cuts a spotlight around it, and shows
an instruction card. The user completes the step by satisfying a condition, and
the overlay advances. The spotlight animates smoothly between targets, and
navigation between pages is handled automatically.
New users see a prompt on first load asking if they want a guided tour. This
starts the Welcome stage, which introduces Profilarr and ends at the onboarding
page where users can run any stage at their own pace. The prompt is driven by a
database flag (onboarding_shown on general_settings) that's set whether the
user accepts or dismisses.
Steps
A step is a single instruction: spotlight an element, show a card, and wait for the user to do something. Steps are the building blocks of everything else.
interface Step {
id: string;
route?: string | { resolve: string };
target?: string;
title: string;
body: string;
position?: Position;
freeInteract?: boolean;
completion: Completion;
}
target matches a data-onboarding attribute in the DOM. If omitted, the
instruction card appears centered with no spotlight.
route auto-navigates when the step becomes active. Can be a static string or
a dynamic resolver (see Dynamic Routes).
position controls where the instruction card sits relative to the target:
above, below, left, right, above-left, above-right, below-left,
below-right. Defaults to below.
freeInteract disables the click-blocking overlay for this step, allowing the
user to interact with the full UI. Used when a step involves opening dropdowns or
interacting beyond the spotlight target.
Completion Types
completion defines how the step advances:
| Type | What happens |
|---|---|
click |
Advances when the spotlight target is clicked. Uses a MutationObserver if the element isn't in the DOM yet. |
route |
Advances when the browser's pathname matches the specified path. |
state |
Advances when a named async check function returns true. Functions are registered in stateChecks.ts. |
manual |
Shows a Continue button on the instruction card. |
When the completion type is not manual, the forward button is hidden from the
progress bar. The user must satisfy the completion condition to advance.
Dynamic Routes
Steps can use dynamic route resolvers for pages that require runtime data (e.g. navigating to a specific database or Arr instance). Instead of a static route string, use an object referencing a named resolver:
route: {
resolve: 'firstDatabaseChanges';
}
Resolvers are registered in src/lib/client/cutscene/routeResolvers.ts. Each
resolver is an async function that returns a path string:
export const routeResolvers: Record<string, () => Promise<string>> = {
firstDatabaseChanges: async () => {
const res = await fetch('/api/v1/databases');
const dbs = await res.json();
return `/databases/${dbs[0].id}/changes`;
}
};
Stages
A stage groups steps into a self-contained lesson that teaches one thing. Stages are independent and can be run on their own from the onboarding page.
interface Stage {
id: string;
name: string;
description: string;
steps: Step[];
silent?: boolean;
prerequisites?: Prerequisite[];
}
silent skips the completion modal when this stage finishes. Used for stages
like Help where a "you're done" modal would be redundant.
prerequisites gates the stage behind runtime conditions. See
Prerequisites below.
Current Stages
| ID | Name | Steps | Prerequisites | Description |
|---|---|---|---|---|
welcome |
Welcome | 3 | What Profilarr is and how it works | |
navigation |
Navigation | 5 | The main sections of the app | |
personalize |
Personalize | 2 | Theme toggle and accent color picker | |
help |
Help | 1 | Introduces the help button (silent) | |
database-link |
Link | 7 | Connect a configuration database | |
database-manage |
Overview | 6 | hasDatabase |
Tabs and features of a connected database |
arr-link |
Link | 5 | Connect a Radarr or Sonarr instance | |
arr-manage |
Overview | 7 | hasArrInstance |
Tabs and features of a connected Arr instance |
arr-sync |
Sync | 7 | hasArrInstance |
Configure what gets synced and when |
arr-upgrades |
Upgrades | 11 | hasArrInstance |
Automated searching for better quality releases |
arr-renames |
Rename | 7 | hasArrInstance |
Automated file and folder renaming |
cf-general |
General | 6 | hasDatabase |
Identity, rename behavior, and references |
cf-conditions |
Conditions | 8 | hasDatabase |
Define what a custom format matches |
cf-testing |
Testing | 7 | hasDevDatabase, parserHealthy |
Verify a format against real release titles |
qp-general |
General | 6 | hasDatabase |
Identity, description, tags, and language |
qp-scoring |
Scoring | 7 | hasDatabase |
Score custom formats and shape upgrade behavior |
qp-qualities |
Qualities | 7 | hasDatabase |
Order, enable, and group qualities |
regex-general |
General | 6 | hasDatabase |
Reusable regex patterns for CF conditions |
delay-general |
General | 6 | hasDatabase |
Name, protocol, delays, and bypass conditions |
media-naming |
Naming | 4 | hasDatabase |
Naming formats and character replacement for files and folders |
media-quality-definitions |
Quality Definitions | 1 | hasDatabase |
File size gates per quality tier |
media-settings |
Media Settings | 5 | hasDatabase |
Propers/repacks handling and file analysis |
settings-general |
General | 7 | App-wide preferences bucket | |
settings-jobs |
Jobs | 4 | The event-driven job queue | |
settings-logs |
Logs | 4 | Read, filter, and download log files | |
settings-backups |
Backups | 4 | Archive viewer and manual operations | |
settings-notifications |
Notifications | 5 | Push alerts to Discord, Telegram, Ntfy, or webhooks | |
settings-security |
Security | 6 | Password, API key, local bypass, sessions |
Prerequisites
A prerequisite gates a stage behind a runtime condition. Before a stage starts, its prerequisites are checked. If any check fails, the cutscene does not start and the user sees an error alert with the prerequisite's message.
interface Prerequisite {
check: string;
message: string;
}
check references a named function in the stateChecks registry
(src/lib/client/cutscene/stateChecks.ts). Each function is async and returns a
boolean.
message is shown via alertStore when the check returns false.
How checks run
The prerequisite runner (src/lib/client/cutscene/prerequisites.ts) collects
prerequisites from the requested stage, runs each check in order, and returns on
the first failure.
Adding a prerequisite
- Add a check function to
stateChecks.ts:
export const stateChecks: Record<string, () => Promise<boolean>> = {
hasDatabase: async () => {
const res = await fetch('/api/v1/databases');
if (!res.ok) return false;
const data = await res.json();
return data.length > 0;
}
};
- Add the prerequisite to the stage definition:
prerequisites: [{ check: 'hasDatabase', message: 'You need at least one connected database.' }];
The same check function can be referenced by multiple stages.
Groups
Groups organize stages visually on the onboarding page. They have no runtime behavior; stages within a group run independently.
interface StageGroup {
name: string;
description: string;
stages: string[];
}
Groups are defined in definitions/index.ts:
export const GROUPS: StageGroup[] = [
{
name: 'Getting Started',
description: 'Learn the basics of Profilarr',
stages: ['welcome', 'navigation', 'personalize', 'help']
},
{
name: 'Databases',
description: 'Connect and manage configuration databases',
stages: ['database-link', 'database-manage']
}
];
Current Groups
| Name | Stages |
|---|---|
| Getting Started | Welcome, Navigation, Personalize, Help |
| Databases | Connect a Database, Managing a Database |
| Arr Instances | Connect an Arr Instance, Managing an Arr Instance |
| Media Management | Naming, Quality Definitions, Media Settings |
| Custom Formats | General, Conditions, Testing |
| Quality Profiles | General, Scoring, Qualities |
| Regular Expressions | General |
| Delay Profiles | General |
| Settings | General, Jobs, Logs, Backups, Notifications, Security |
Adding Content
Adding a step to an existing stage: add an entry to the stage's steps
array and tag the target element with data-onboarding:
<div data-onboarding="my-target">...</div>
If the step uses state completion, register the check function in
src/lib/client/cutscene/stateChecks.ts.
If the step needs a dynamic route, register a resolver in
src/lib/client/cutscene/routeResolvers.ts.
Adding a stage: create a file in src/lib/client/cutscene/definitions/stages/
and register it in definitions/index.ts:
// definitions/stages/my-stage.ts
import type { Stage } from '../../types.ts';
export const myStage: Stage = {
id: 'my-stage',
name: 'My Stage',
description: 'What this stage teaches',
steps: [
{
id: 'first-step',
target: 'my-target',
title: 'Do This',
body: 'Click the thing to continue.',
position: 'right',
completion: { type: 'click' }
}
]
};
// definitions/index.ts
import { myStage } from './stages/my-stage.ts';
export const STAGES: Record<string, Stage> = {
// ...existing
'my-stage': myStage
};
Add the stage to a group in GROUPS so it appears on the onboarding page.
Overlay Engine
The overlay uses two layers working together:
-
SVG mask handles the visual darkening. A full-screen rectangle is masked with a rounded cutout where the target element is. This layer has
pointer-events: noneeverywhere; it's purely visual. -
CSS clip-path handles click blocking. A separate div with
pointer-events: autouses a clip-path polygon that covers everything except the cutout area. Clicks in the cutout pass through to the real UI beneath.
Both layers are needed because SVG masks don't affect pointer events. The
spotlight finds its target via querySelector('[data-onboarding="..."]') and
recalculates on scroll, resize, and navigation.
Store & Persistence
Runtime state is managed by a Svelte writable store. Progress (active stage and current step) is saved to localStorage on every state change, so refreshing mid-walkthrough picks up where the user left off.
The only server-side persistence is the onboarding_shown flag on
general_settings (migration 058). This controls whether the first-run prompt
appears. Once set, it's never unset.
Cutscenes started from the onboarding page are flagged as manualStart. When a
manually started cutscene completes, the completion modal appears offering to
return to the onboarding page. Cutscenes started from the first-run prompt skip
this.
Onboarding Page
/onboarding shows stages organized by group. Each group displays its name,
description, and the stages within it. Users can start any individual stage.
The help button (parrot) includes an "Onboarding" link to this page on desktop.
Mobile
Cutscene is disabled on screens below 768px. The overlay, prompt, and completion modal don't mount. The onboarding link is hidden in the mobile help button.
The current stages spotlight sidebar elements which aren't visible on mobile. Supporting mobile would require either different stages or a different spotlight approach for the bottom navigation.