From 5cc142a12ffd1cc0d09edc65be906e02b3a3ec00 Mon Sep 17 00:00:00 2001 From: santiagosayshey Date: Tue, 7 Apr 2026 05:11:11 +0930 Subject: [PATCH] feat: enable onboarding flag, add cutscene stages for databases and arr instances (#407) --- deno.json | 1 + docs/architecture/cutscene.md | 198 +++-- src/lib/client/alerts/AlertContainer.svelte | 4 +- src/lib/client/cutscene/CutsceneCard.svelte | 44 +- .../client/cutscene/CutsceneOverlay.svelte | 82 +- .../client/cutscene/CutsceneProgress.svelte | 52 ++ src/lib/client/cutscene/CutscenePrompt.svelte | 2 +- src/lib/client/cutscene/definitions/index.ts | 47 +- .../definitions/pipelines/getting-started.ts | 8 - .../cutscene/definitions/stages/arrs/link.ts | 51 ++ .../definitions/stages/arrs/overview.ts | 76 ++ .../definitions/stages/arrs/renames.ts | 67 ++ .../cutscene/definitions/stages/arrs/sync.ts | 70 ++ .../definitions/stages/arrs/upgrades.ts | 102 +++ .../{databases.ts => databases/link.ts} | 41 +- .../definitions/stages/databases/overview.ts | 67 ++ .../stages/{ => getting-started}/help.ts | 2 +- .../stages/getting-started/navigation.ts | 52 ++ .../{ => getting-started}/personalize.ts | 2 +- .../stages/getting-started/welcome.ts | 29 + .../cutscene/definitions/stages/welcome.ts | 95 --- src/lib/client/cutscene/prerequisites.ts | 21 + src/lib/client/cutscene/routeResolvers.ts | 62 ++ src/lib/client/cutscene/stateChecks.ts | 16 +- src/lib/client/cutscene/store.ts | 86 +- src/lib/client/cutscene/types.ts | 13 +- src/lib/client/stores/dirty.ts | 6 +- src/lib/client/ui/actions/ActionButton.svelte | 2 + src/lib/client/ui/card/ExpandableCard.svelte | 43 + src/lib/client/ui/help/HelpButton.svelte | 21 +- .../client/ui/navigation/navbar/navbar.svelte | 13 +- .../client/ui/navigation/pageNav/group.svelte | 7 +- .../ui/navigation/pageNav/groupItem.svelte | 5 +- .../ui/navigation/pageNav/pageNav.svelte | 129 +-- src/lib/client/ui/navigation/tabs/Tabs.svelte | 2 + src/lib/client/ui/state/EmptyState.svelte | 17 +- src/lib/shared/features.ts | 2 +- src/routes/+layout.svelte | 3 +- src/routes/arr/+page.svelte | 8 +- src/routes/arr/[id]/+layout.svelte | 18 +- .../rename/components/RenameSettings.svelte | 8 +- src/routes/arr/[id]/sync/+page.svelte | 4 +- .../[id]/sync/components/DelayProfiles.svelte | 1 + .../sync/components/MediaManagement.svelte | 2 + .../sync/components/QualityProfiles.svelte | 1 + .../[id]/sync/components/SyncFooter.svelte | 3 +- src/routes/arr/[id]/upgrades/+page.svelte | 2 +- .../upgrades/components/CoreSettings.svelte | 4 +- .../upgrades/components/FilterSettings.svelte | 31 +- src/routes/arr/components/InstanceForm.svelte | 133 +-- src/routes/databases/+page.svelte | 8 +- src/routes/databases/[id]/+layout.svelte | 15 +- .../databases/components/InstanceForm.svelte | 16 +- src/routes/onboarding/+page.svelte | 251 ++---- src/routes/settings/+page.svelte | 18 +- src/routes/settings/general/+page.svelte | 801 +++++++++--------- svelte.config.js | 3 +- 57 files changed, 1713 insertions(+), 1154 deletions(-) create mode 100644 src/lib/client/cutscene/CutsceneProgress.svelte delete mode 100644 src/lib/client/cutscene/definitions/pipelines/getting-started.ts create mode 100644 src/lib/client/cutscene/definitions/stages/arrs/link.ts create mode 100644 src/lib/client/cutscene/definitions/stages/arrs/overview.ts create mode 100644 src/lib/client/cutscene/definitions/stages/arrs/renames.ts create mode 100644 src/lib/client/cutscene/definitions/stages/arrs/sync.ts create mode 100644 src/lib/client/cutscene/definitions/stages/arrs/upgrades.ts rename src/lib/client/cutscene/definitions/stages/{databases.ts => databases/link.ts} (61%) create mode 100644 src/lib/client/cutscene/definitions/stages/databases/overview.ts rename src/lib/client/cutscene/definitions/stages/{ => getting-started}/help.ts (87%) create mode 100644 src/lib/client/cutscene/definitions/stages/getting-started/navigation.ts rename src/lib/client/cutscene/definitions/stages/{ => getting-started}/personalize.ts (92%) create mode 100644 src/lib/client/cutscene/definitions/stages/getting-started/welcome.ts delete mode 100644 src/lib/client/cutscene/definitions/stages/welcome.ts create mode 100644 src/lib/client/cutscene/prerequisites.ts create mode 100644 src/lib/client/cutscene/routeResolvers.ts create mode 100644 src/lib/client/ui/card/ExpandableCard.svelte diff --git a/deno.json b/deno.json index fefb01aa..57cd3059 100644 --- a/deno.json +++ b/deno.json @@ -37,6 +37,7 @@ "dev:parser": "cd src/services/parser && dotnet watch run --urls http://localhost:5000", "build": "APP_BASE_PATH=./dist/build deno run -A npm:vite build && deno compile --no-check --allow-net --allow-read --allow-write --allow-env --allow-ffi --allow-run --allow-sys --target x86_64-unknown-linux-gnu --output dist/build/profilarr dist/build/mod.ts", "preview": "PORT=6869 HOST=0.0.0.0 APP_BASE_PATH=./dist/dev PARSER_HOST=localhost PARSER_PORT=5000 ./dist/build/profilarr", + "reset:onboarding": "sqlite3 dist/dev/data/profilarr.db \"UPDATE general_settings SET onboarding_shown = 0\"", "clean:dev": "rm -rf dist/dev", "format": "prettier --write .", "lint": "prettier --check . && deno lint", diff --git a/docs/architecture/cutscene.md b/docs/architecture/cutscene.md index ea3d57a0..bc6338b4 100644 --- a/docs/architecture/cutscene.md +++ b/docs/architecture/cutscene.md @@ -5,8 +5,10 @@ - [Overview](#overview) - [Steps](#steps) - [Completion Types](#completion-types) + - [Dynamic Routes](#dynamic-routes) - [Stages](#stages) -- [Pipelines](#pipelines) +- [Prerequisites](#prerequisites) +- [Groups](#groups) - [Adding Content](#adding-content) - [Overlay Engine](#overlay-engine) - [Store & Persistence](#store--persistence) @@ -31,10 +33,11 @@ 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 is -driven by a database flag (`onboarding_shown` on `general_settings`) that's set -whether the user accepts or dismisses. Users can replay any stage or pipeline -later from the `/onboarding` page, accessible via the help button. +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 @@ -44,7 +47,7 @@ the user to do something. Steps are the building blocks of everything else. ```ts interface Step { id: string; - route?: string; + route?: string | { resolve: string }; target?: string; title: string; body: string; @@ -57,8 +60,8 @@ interface Step { `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. If the user is already on -that route, nothing happens. +`route` auto-navigates when the step becomes active. Can be a static string or +a dynamic resolver (see [Dynamic Routes](#dynamic-routes)). `position` controls where the instruction card sits relative to the target: `above`, `below`, `left`, `right`, `above-left`, `above-right`, `below-left`, @@ -79,6 +82,34 @@ interacting beyond the spotlight target. | `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: + +```ts +route: { + resolve: 'firstDatabaseChanges'; +} +``` + +Resolvers are registered in `src/lib/client/cutscene/routeResolvers.ts`. Each +resolver is an async function that returns a path string: + +```ts +export const routeResolvers: Record Promise> = { + 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 @@ -91,42 +122,117 @@ interface Stage { description: string; steps: Step[]; silent?: boolean; + prerequisites?: Prerequisite[]; } ``` -`silent` skips the completion modal when this stage finishes as the last stage in -a run. Used for stages like Help where a "you're done" modal would be redundant. +`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](#prerequisites) below. ### Current Stages -| ID | Name | Steps | Description | -| ------------- | ----------- | ----- | ---------------------------------------------------------------------- | -| `welcome` | Welcome | 10 | Sidebar walkthrough: what Profilarr is and what each section does | -| `personalize` | Personalize | 2 | Theme toggle and accent color picker | -| `databases` | Databases | 6 | Linking a database: form fields, PAT, conflict strategy, sync settings | -| `help` | Help | 1 | Introduces the help button (silent) | +| 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 | -## Pipelines +## Prerequisites -A pipeline chains stages together in order. When one stage completes, the next -begins automatically. +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. ```ts -interface Pipeline { - id: string; +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 + +1. Add a check function to `stateChecks.ts`: + +```ts +export const stateChecks: Record Promise> = { + hasDatabase: async () => { + const res = await fetch('/api/v1/databases'); + if (!res.ok) return false; + const data = await res.json(); + return data.length > 0; + } +}; +``` + +2. Add the prerequisite to the stage definition: + +```ts +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. + +```ts +interface StageGroup { name: string; description: string; stages: string[]; } ``` -The `stages` array contains stage IDs referencing entries in the stage registry. +Groups are defined in `definitions/index.ts`: -### Current Pipelines +```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'] + } +]; +``` -| ID | Name | Stages | Description | -| ----------------- | --------------- | ---------------------------------------- | -------------------------------------------- | -| `getting-started` | Getting Started | Welcome → Personalize → Databases → Help | First-run onboarding covering the essentials | +### 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 | ## Adding Content @@ -138,16 +244,10 @@ array and tag the target element with `data-onboarding`: ``` If the step uses `state` completion, register the check function in -`src/lib/client/cutscene/stateChecks.ts`: +`src/lib/client/cutscene/stateChecks.ts`. -```ts -export const stateChecks: Record Promise> = { - myCheck: async () => { - const res = await fetch('/api/v1/...'); - return res.ok; - } -}; -``` +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`: @@ -183,22 +283,7 @@ export const STAGES: Record = { }; ``` -The stage automatically appears on the `/onboarding` page. - -**Adding a pipeline**: create a file in `definitions/pipelines/` and register it -in `definitions/index.ts`: - -```ts -// definitions/pipelines/my-pipeline.ts -import type { Pipeline } from '../../types.ts'; - -export const myPipeline: Pipeline = { - id: 'my-pipeline', - name: 'My Pipeline', - description: 'A guided experience', - stages: ['stage-one', 'stage-two', 'stage-three'] -}; -``` +Add the stage to a group in `GROUPS` so it appears on the onboarding page. ## Overlay Engine @@ -218,9 +303,9 @@ recalculates on scroll, resize, and navigation. ## Store & Persistence -Runtime state is managed by a Svelte writable store. Progress (active pipeline, -current stage/step) is saved to localStorage on every state change, so -refreshing mid-walkthrough picks up where the user left off. +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 @@ -233,9 +318,8 @@ this. ## Onboarding Page -`/onboarding` lists all registered pipelines and stages. Users can start a full -pipeline or replay an individual stage. The page supports table/card view toggle -and search by name or description. +`/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. diff --git a/src/lib/client/alerts/AlertContainer.svelte b/src/lib/client/alerts/AlertContainer.svelte index b59e8c0b..dc4205e8 100644 --- a/src/lib/client/alerts/AlertContainer.svelte +++ b/src/lib/client/alerts/AlertContainer.svelte @@ -18,9 +18,9 @@
- import { ArrowRight, X } from 'lucide-svelte'; + import { X } from 'lucide-svelte'; import Button from '$ui/button/Button.svelte'; + import CutsceneProgress from './CutsceneProgress.svelte'; export let title: string; export let body: string; - export let showContinue: boolean = false; - export let onContinue: (() => void) | undefined = undefined; + export let onBack: (() => void) | undefined = undefined; + export let onForward: (() => void) | undefined = undefined; export let onCancel: (() => void) | undefined = undefined; + export let showBack: boolean = true; export let currentStep: number = 0; export let totalSteps: number = 0; @@ -19,8 +21,6 @@ ]; $: cancelTooltip = cancelQuips[Math.floor(Math.random() * cancelQuips.length)]; - $: showProgress = totalSteps > 1; - $: progressPercent = totalSteps > 0 ? ((currentStep + 1) / totalSteps) * 100 : 0;
-
-

- {title} -

- {#if showProgress} - - {currentStep + 1}/{totalSteps} - - {/if} -
+

+ {title} +

{#if onCancel}
- {#if showProgress} -
-
-
- {/if} - {#if showContinue && onContinue} -
-
- {/if} +
diff --git a/src/lib/client/cutscene/CutsceneOverlay.svelte b/src/lib/client/cutscene/CutsceneOverlay.svelte index 91ca0924..66b045e1 100644 --- a/src/lib/client/cutscene/CutsceneOverlay.svelte +++ b/src/lib/client/cutscene/CutsceneOverlay.svelte @@ -4,7 +4,8 @@ import { fade, fly } from 'svelte/transition'; import { cutscene } from './store'; import { setupCompletion, teardownCompletion } from './completions.ts'; - import { STAGES, PIPELINES } from './definitions/index.ts'; + import { STAGES } from './definitions/index.ts'; + import { routeResolvers } from './routeResolvers.ts'; import CutsceneCard from './CutsceneCard.svelte'; let targetRect: DOMRect | null = null; @@ -17,27 +18,10 @@ $: step = $currentStep; $: active = state.active; - // Compute overall progress across pipeline or single stage + $: isFirstStep = state.stepIndex === 0; + $: progressInfo = (() => { if (!state.active || !state.stageId) return { current: 0, total: 0 }; - - if (state.pipelineId) { - const pipeline = PIPELINES[state.pipelineId]; - if (!pipeline) return { current: 0, total: 0 }; - - let total = 0; - let current = 0; - for (const sid of pipeline.stages) { - const s = STAGES[sid]; - if (!s) continue; - if (sid === state.stageId) { - current = total + state.stepIndex; - } - total += s.steps.length; - } - return { current, total }; - } - const stage = STAGES[state.stageId]; if (!stage) return { current: 0, total: 0 }; return { current: state.stepIndex, total: stage.steps.length }; @@ -77,6 +61,11 @@ requestAnimationFrame(frame); } + function isInViewport(el: Element): boolean { + const rect = el.getBoundingClientRect(); + return rect.top >= 0 && rect.bottom <= window.innerHeight; + } + function findTarget(): void { if (!step?.target) { targetRect = null; @@ -84,6 +73,12 @@ } const el = document.querySelector(`[data-onboarding="${step.target}"]`); if (el) { + if (!isInViewport(el)) { + el.scrollIntoView({ block: 'center', behavior: 'smooth' }); + // Re-measure after scroll settles + setTimeout(() => findTarget(), 400); + return; + } const rect = el.getBoundingClientRect(); const newRect = { x: rect.left - PAD, @@ -115,14 +110,19 @@ let lastStepId: string | null = null; let cardReady = false; + async function resolveRoute(route: string | { resolve: string }): Promise { + if (typeof route === 'string') return route; + const resolver = routeResolvers[route.resolve]; + if (!resolver) return '/'; + return resolver(); + } + $: if (step && step.id !== lastStepId && typeof window !== 'undefined') { lastStepId = step.id; cardReady = false; teardownCompletion(); setupCompletion(step, () => cutscene.advance()); - // Navigate if step requires a specific route - const needsNav = step.route && window.location.pathname !== step.route; const afterNav = () => { tick().then(() => { requestAnimationFrame(() => { @@ -137,8 +137,15 @@ }); }; - if (needsNav) { - goto(step.route!).then(afterNav); + // Navigate if step requires a specific route + if (step.route) { + resolveRoute(step.route).then((resolved) => { + if (window.location.pathname !== resolved) { + goto(resolved).then(afterNav); + } else { + afterNav(); + } + }); } else { afterNav(); } @@ -224,10 +231,14 @@ } } - function handleContinue(): void { + function handleForward(): void { cutscene.advance(); } + function handleBack(): void { + cutscene.goBack(); + } + function handleCancel(): void { teardownCompletion(); cutscene.cancel(); @@ -239,10 +250,26 @@ } } + // Prevent user scroll while cutscene is active (but allow programmatic scrollIntoView) + function preventScroll(e: Event): void { + e.preventDefault(); + } + $: if (typeof window !== 'undefined') { + if (active) { + window.addEventListener('wheel', preventScroll, { passive: false }); + window.addEventListener('touchmove', preventScroll, { passive: false }); + } else { + window.removeEventListener('wheel', preventScroll); + window.removeEventListener('touchmove', preventScroll); + } + } + onDestroy(() => { teardownCompletion(); observer?.disconnect(); if (typeof window !== 'undefined') { + window.removeEventListener('wheel', preventScroll); + window.removeEventListener('touchmove', preventScroll); window.removeEventListener('scroll', onScroll, true); window.removeEventListener('resize', updateDimensions); } @@ -304,9 +331,10 @@ diff --git a/src/lib/client/cutscene/CutsceneProgress.svelte b/src/lib/client/cutscene/CutsceneProgress.svelte new file mode 100644 index 00000000..ceccdcd4 --- /dev/null +++ b/src/lib/client/cutscene/CutsceneProgress.svelte @@ -0,0 +1,52 @@ + + +
+
+ {currentStep + 1}/{totalSteps} +
+
+ {#if showBack} + + {/if} +
+
+
+ {#if onForward} + + {/if} +
+
diff --git a/src/lib/client/cutscene/CutscenePrompt.svelte b/src/lib/client/cutscene/CutscenePrompt.svelte index 1ee181f1..0981401c 100644 --- a/src/lib/client/cutscene/CutscenePrompt.svelte +++ b/src/lib/client/cutscene/CutscenePrompt.svelte @@ -12,7 +12,7 @@ function start(): void { cutscene.dismiss(); - cutscene.startPipeline('getting-started', false); + cutscene.startStage('welcome', false); } function skip(): void { diff --git a/src/lib/client/cutscene/definitions/index.ts b/src/lib/client/cutscene/definitions/index.ts index 8440976d..8cf305b8 100644 --- a/src/lib/client/cutscene/definitions/index.ts +++ b/src/lib/client/cutscene/definitions/index.ts @@ -1,17 +1,44 @@ -import type { Stage, Pipeline } from '../types.ts'; -import { welcomeStage } from './stages/welcome.ts'; -import { personalizeStage } from './stages/personalize.ts'; -import { databasesStage } from './stages/databases.ts'; -import { helpStage } from './stages/help.ts'; -import { gettingStartedPipeline } from './pipelines/getting-started.ts'; +import type { Stage, StageGroup } from '../types.ts'; +import { welcomeStage } from './stages/getting-started/welcome.ts'; +import { navigationStage } from './stages/getting-started/navigation.ts'; +import { personalizeStage } from './stages/getting-started/personalize.ts'; +import { helpStage } from './stages/getting-started/help.ts'; +import { databaseLinkStage } from './stages/databases/link.ts'; +import { databaseOverviewStage } from './stages/databases/overview.ts'; +import { arrLinkStage } from './stages/arrs/link.ts'; +import { arrOverviewStage } from './stages/arrs/overview.ts'; +import { arrSyncStage } from './stages/arrs/sync.ts'; +import { arrUpgradesStage } from './stages/arrs/upgrades.ts'; +import { arrRenameStage } from './stages/arrs/renames.ts'; export const STAGES: Record = { welcome: welcomeStage, + navigation: navigationStage, personalize: personalizeStage, - databases: databasesStage, + 'database-link': databaseLinkStage, + 'database-manage': databaseOverviewStage, + 'arr-link': arrLinkStage, + 'arr-manage': arrOverviewStage, + 'arr-sync': arrSyncStage, + 'arr-upgrades': arrUpgradesStage, + 'arr-renames': arrRenameStage, help: helpStage }; -export const PIPELINES: Record = { - 'getting-started': gettingStartedPipeline -}; +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'] + }, + { + name: 'Arr Instances', + description: 'Connect and manage Radarr/Sonarr instances', + stages: ['arr-link', 'arr-manage', 'arr-sync', 'arr-upgrades', 'arr-renames'] + } +]; diff --git a/src/lib/client/cutscene/definitions/pipelines/getting-started.ts b/src/lib/client/cutscene/definitions/pipelines/getting-started.ts deleted file mode 100644 index 0c040647..00000000 --- a/src/lib/client/cutscene/definitions/pipelines/getting-started.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { Pipeline } from '../../types.ts'; - -export const gettingStartedPipeline: Pipeline = { - id: 'getting-started', - name: 'Getting Started', - description: 'Learn the basics of Profilarr', - stages: ['welcome', 'personalize', 'databases', 'help'] -}; diff --git a/src/lib/client/cutscene/definitions/stages/arrs/link.ts b/src/lib/client/cutscene/definitions/stages/arrs/link.ts new file mode 100644 index 00000000..d5f5b780 --- /dev/null +++ b/src/lib/client/cutscene/definitions/stages/arrs/link.ts @@ -0,0 +1,51 @@ +import type { Stage } from '$cutscene/types.ts'; + +export const arrLinkStage: Stage = { + id: 'arr-link', + name: 'Link', + description: 'Connect a Radarr or Sonarr instance', + steps: [ + { + id: 'arr-explain', + target: 'nav-arrs', + title: 'Arrs', + body: "Now let's connect an Arr instance. Arrs are your Radarr and Sonarr instances that Profilarr manages. Once connected, you can push configurations directly to them. Click Arrs to continue.", + position: 'right', + completion: { type: 'click' } + }, + { + id: 'arr-add', + route: '/arr', + target: 'arr-add', + title: 'Add an Instance', + body: 'Click here to start connecting a new Arr instance.', + position: 'below-left', + completion: { type: 'click' } + }, + { + id: 'arr-type', + route: '/arr/new', + target: 'arr-type', + title: 'Instance Type', + body: 'First, select whether this is a Radarr (movies) or Sonarr (TV shows) instance.', + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'arr-connection', + target: 'arr-connection', + title: 'Connection Details', + body: "Give your instance a name, enter its URL, and paste the API key. You can find the API key in your Arr's Settings > General page. If you're running both Profilarr and your Arr in Docker, use the container name as the host (e.g. http://radarr:7878).", + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'arr-save', + target: 'arr-save', + title: 'Save', + body: "Once you've filled in the connection details, hit Save to connect the instance. Profilarr will verify the connection and add it to your setup.", + position: 'below-left', + completion: { type: 'manual' } + } + ] +}; diff --git a/src/lib/client/cutscene/definitions/stages/arrs/overview.ts b/src/lib/client/cutscene/definitions/stages/arrs/overview.ts new file mode 100644 index 00000000..2a51f8b5 --- /dev/null +++ b/src/lib/client/cutscene/definitions/stages/arrs/overview.ts @@ -0,0 +1,76 @@ +import type { Stage } from '$cutscene/types.ts'; + +export const arrOverviewStage: Stage = { + id: 'arr-manage', + name: 'Overview', + description: 'Tabs and features of a connected Arr instance', + prerequisites: [ + { + check: 'hasArrInstance', + message: + 'You need at least one connected Arr instance to start this stage. Follow the "Link" stage under Arr Instances from the onboarding page to connect one.' + } + ], + steps: [ + { + id: 'arr-nav-sync', + route: { resolve: 'firstArrSync' }, + target: 'arr-tab-sync', + title: 'Sync', + body: "The Sync tab is where you configure what gets pushed to this instance. You'll set up media management, delay profiles, and quality profiles here. We'll walk through this in a dedicated stage later.", + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'arr-nav-library', + route: { resolve: 'firstArrLibrary' }, + target: 'arr-tab-library', + title: 'Library', + body: "The Library tab shows everything on your Arr instance: movies for Radarr, series for Sonarr. You can browse items, see their quality profiles, custom format scores, and whether they've hit their quality cutoff.", + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'arr-nav-upgrades', + route: { resolve: 'firstArrUpgrades' }, + target: 'arr-tab-upgrades', + title: 'Upgrades', + body: "The Upgrades tab lets you configure automated searches for better quality releases. Set up filters to target specific items and schedules to run searches automatically. We'll cover this in a dedicated stage later.", + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'arr-nav-renames', + route: { resolve: 'firstArrRename' }, + target: 'arr-tab-renames', + title: 'Renames', + body: "The Renames tab handles bulk file and folder renaming to match your naming conventions. You can preview changes with a dry run before applying them. We'll cover this in a dedicated stage later.", + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'arr-nav-logs', + route: { resolve: 'firstArrLogs' }, + target: 'arr-tab-logs', + title: 'Logs', + body: 'The Logs tab shows activity from this Arr instance. You can filter by log level and search for specific entries.', + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'arr-nav-settings', + route: { resolve: 'firstArrSettings' }, + target: 'arr-tab-settings', + title: 'Settings', + body: 'The Settings tab lets you update connection details, configure library refresh intervals, set up automatic cleanup of stale configurations, and remove the instance if needed.', + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'arr-summary', + title: "You're connected", + body: "You've learned how to connect an Arr instance and explored the tabs where you'll manage your setup. The next steps are configuring sync to push configurations to your instance, setting up automated upgrades to search for better releases, and configuring renames to keep your files organized. Each of these has a dedicated walkthrough you can run from the onboarding page.", + completion: { type: 'manual' } + } + ] +}; diff --git a/src/lib/client/cutscene/definitions/stages/arrs/renames.ts b/src/lib/client/cutscene/definitions/stages/arrs/renames.ts new file mode 100644 index 00000000..78e8ed3d --- /dev/null +++ b/src/lib/client/cutscene/definitions/stages/arrs/renames.ts @@ -0,0 +1,67 @@ +import type { Stage } from '$cutscene/types.ts'; + +export const arrRenameStage: Stage = { + id: 'arr-renames', + name: 'Rename', + description: 'Automated file and folder renaming', + prerequisites: [ + { + check: 'hasArrInstance', + message: + 'You need at least one connected Arr instance to start this stage. Follow the "Link" stage under Arr Instances from the onboarding page to connect one.' + } + ], + steps: [ + { + id: 'rename-why', + route: { resolve: 'firstArrRename' }, + title: 'Why Rename?', + body: "Arrs name files when they're first grabbed, but file names can fall out of sync over time. TBA episodes get placeholder names that need updating once the real title is known. Switching naming conventions or syncing new naming settings from a database leaves existing files with the old format. Rename catches all of this by comparing every file against your current naming format and fixing anything that doesn't match.", + completion: { type: 'manual' } + }, + { + id: 'rename-what', + title: 'What is Rename?', + body: "Rename scans your library, compares each file name against your naming format, and renames anything that doesn't match. You can preview changes with a dry run before applying them.", + completion: { type: 'manual' } + }, + { + id: 'rename-folders', + target: 'rename-folders', + title: 'Rename Folders', + body: 'When enabled, folders are renamed alongside files. When off, only file names are updated.', + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'rename-summary', + target: 'rename-summary', + title: 'Summary Notifications', + body: 'Controls how rename notifications are sent. Summary mode sends a compact notification with a total count. Detailed mode lists every renamed file individually.', + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'rename-schedule', + target: 'rename-schedule', + title: 'Schedule', + body: 'How often renames run automatically. The default is daily at midnight. Like upgrades, this uses a cron expression.', + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'rename-ignore-tag', + target: 'rename-ignore-tag', + title: 'Ignore Tag', + body: "Items tagged with this value in your Arr instance will be skipped during renames. Useful for excluding items you've manually named or don't want touched.", + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'rename-summary-step', + title: 'Ready to Rename', + body: 'Start with a dry run from the button above to preview what would be renamed before running it live.', + completion: { type: 'manual' } + } + ] +}; diff --git a/src/lib/client/cutscene/definitions/stages/arrs/sync.ts b/src/lib/client/cutscene/definitions/stages/arrs/sync.ts new file mode 100644 index 00000000..0ae624b2 --- /dev/null +++ b/src/lib/client/cutscene/definitions/stages/arrs/sync.ts @@ -0,0 +1,70 @@ +import type { Stage } from '$cutscene/types.ts'; + +export const arrSyncStage: Stage = { + id: 'arr-sync', + name: 'Sync', + description: 'Configure what gets synced and when', + prerequisites: [ + { + check: 'hasArrInstance', + message: + 'You need at least one connected Arr instance to start this stage. Follow the "Link" stage under Arr Instances from the onboarding page to connect one.' + } + ], + steps: [ + { + id: 'sync-overview', + route: { resolve: 'firstArrSync' }, + title: 'What is Sync?', + body: "Sync pushes configurations from your databases into your Arr instances. Profilarr stores everything in its own format, then compiles it into what Radarr or Sonarr expects. This lets Profilarr do things the Arrs can't natively, like reusing regular expressions across custom formats.", + completion: { type: 'manual' } + }, + { + id: 'sync-media-management', + target: 'sync-media-management', + title: 'Media Management', + body: 'Media management covers naming conventions, quality definitions, and media settings. This needs to be configured and saved before quality profiles can be synced.', + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'sync-delay-profiles', + target: 'sync-delay-profiles', + title: 'Delay Profiles', + body: 'Delay profiles control protocol preferences, delays, and score gates. Like media management, these must be configured and saved before quality profiles.', + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'sync-quality-profiles', + target: 'sync-quality-profiles', + title: 'Quality Profiles', + body: "Quality profiles are the core of your configuration. Select which profiles to sync from your connected databases. Custom formats don't need to be selected individually; if a custom format is part of a quality profile, it syncs automatically.", + position: 'above', + completion: { type: 'manual' } + }, + { + id: 'sync-trigger', + target: 'sync-trigger', + title: 'Triggers', + body: 'Each section has its own trigger that controls when sync happens. Manual means you click sync yourself. On Pull syncs automatically when new changes arrive from a database. Schedule runs on a cron expression you define.', + position: 'below-right', + freeInteract: true, + completion: { type: 'manual' } + }, + { + id: 'sync-how-it-works', + target: 'sync-how-it-works', + title: 'How it Works', + body: 'For a deeper breakdown of sync mechanics, triggers, cron expressions, and setup order, click this button anytime. It covers everything in detail.', + position: 'below-left', + completion: { type: 'manual' } + }, + { + id: 'sync-summary', + title: 'Ready to Sync', + body: 'Configure from top to bottom: media management and delay profiles first, then quality profiles. Custom formats come along with their quality profiles automatically. Each section can have its own trigger, so you can mix manual and automatic syncing.', + completion: { type: 'manual' } + } + ] +}; diff --git a/src/lib/client/cutscene/definitions/stages/arrs/upgrades.ts b/src/lib/client/cutscene/definitions/stages/arrs/upgrades.ts new file mode 100644 index 00000000..4a8b54c4 --- /dev/null +++ b/src/lib/client/cutscene/definitions/stages/arrs/upgrades.ts @@ -0,0 +1,102 @@ +import type { Stage } from '$cutscene/types.ts'; + +export const arrUpgradesStage: Stage = { + id: 'arr-upgrades', + name: 'Upgrades', + description: 'Automated searching for better quality releases', + prerequisites: [ + { + check: 'hasArrInstance', + message: + 'You need at least one connected Arr instance to start this stage. Follow the "Link" stage under Arr Instances from the onboarding page to connect one.' + } + ], + steps: [ + { + id: 'upgrades-why', + route: { resolve: 'firstArrUpgrades' }, + title: 'Why Upgrades?', + body: 'Arrs only search for releases when you first add something, then rely on RSS feeds to find upgrades. This works most of the time, but misses edge cases: your best indexers might have been down when a better release appeared, you might switch quality profiles, or a profile update might change what counts as an upgrade. Upgrades fills that gap by actively searching your library on a schedule.', + completion: { type: 'manual' } + }, + { + id: 'upgrades-filters', + target: 'upgrades-filters', + title: 'Filters', + body: 'Filters define which items in your library are eligible for upgrade searching. You can create multiple filters for different purposes: one for old movies below cutoff, another for recent additions, etc.', + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'upgrades-add-filter', + target: 'upgrades-add-filter', + title: 'Add a Filter', + body: 'Click to create your first filter. This opens the filter editor where you can configure rules and settings.', + position: 'below-left', + completion: { type: 'click' } + }, + { + id: 'upgrades-filter-rules', + target: 'upgrades-filter-rules', + title: 'Filter Rules', + body: 'Rules match items by field: title, year, rating, status, tags, and more. Combine rules with "All" (AND) or "Any" (OR) logic. Groups can be nested for complex conditions. For example: status is Released AND (rating > 7 OR year > 2020).', + position: 'above', + freeInteract: true, + completion: { type: 'manual' } + }, + { + id: 'upgrades-cutoff', + target: 'upgrades-cutoff', + title: 'Cutoff %', + body: 'This sets the score threshold for the "cutoff met" field. If your quality profile\'s cutoff score is 90 and you set this to 80%, items scoring below 72 are considered below cutoff. Pair it with a "cutoff met is false" rule to target items that haven\'t reached your quality goals.', + position: 'above', + completion: { type: 'manual' } + }, + { + id: 'upgrades-method', + target: 'upgrades-method', + title: 'Method', + body: 'The selector determines how items are prioritized from the matched set. Oldest targets items that have been waiting the longest. Lowest Score finds items furthest from your quality goals. Random spreads searches evenly.', + position: 'above', + completion: { type: 'manual' } + }, + { + id: 'upgrades-count', + target: 'upgrades-count', + title: 'Count', + body: 'How many items to search per run. This is capped based on your schedule: more frequent runs means a lower max. The cap exists because indexers rate limit search requests. Profilarr calculates the safe maximum automatically.', + position: 'above', + completion: { type: 'manual' } + }, + { + id: 'upgrades-cooldown', + target: 'upgrades-cooldown', + title: 'Cooldown Tag', + body: "After an item is searched, it gets tagged in your Arr instance so it won't be picked again next run. This spreads searches across your entire library over time. When every matched item has been tagged, the tags reset and the cycle starts over.", + position: 'above', + completion: { type: 'manual' } + }, + { + id: 'upgrades-schedule', + target: 'upgrades-schedule', + title: 'The Schedule', + body: 'This controls how often a filter runs. Each time the schedule fires, only one filter executes. This is intentional; indexers rate limit search requests, and hitting them too aggressively can get you throttled or banned.', + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'upgrades-filter-mode', + target: 'upgrades-filter-mode', + title: 'Filter Mode', + body: 'The mode controls which filter runs on each tick. Round robin cycles through your enabled filters in order. Random picks one at random. Either way, only one filter fires per scheduled run.', + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'upgrades-summary', + title: 'Ready to Upgrade', + body: 'Filters target items, selectors prioritize them, the schedule spaces everything out safely. Start with a dry run to test your setup, then enable when you are confident.', + completion: { type: 'manual' } + } + ] +}; diff --git a/src/lib/client/cutscene/definitions/stages/databases.ts b/src/lib/client/cutscene/definitions/stages/databases/link.ts similarity index 61% rename from src/lib/client/cutscene/definitions/stages/databases.ts rename to src/lib/client/cutscene/definitions/stages/databases/link.ts index 9cb00873..3c09219f 100644 --- a/src/lib/client/cutscene/definitions/stages/databases.ts +++ b/src/lib/client/cutscene/definitions/stages/databases/link.ts @@ -1,32 +1,33 @@ -import type { Stage } from '../../types.ts'; +import type { Stage } from '$cutscene/types.ts'; -export const databasesStage: Stage = { - id: 'databases', - name: 'Databases', - description: 'Learn how to link a configuration database', +export const databaseLinkStage: Stage = { + id: 'database-link', + name: 'Link', + description: 'Connect a configuration database', steps: [ { id: 'databases-explain', target: 'nav-databases', title: 'Databases', - body: "Databases are where your configurations come from. They're git repositories containing pre-built custom formats, quality profiles, and more. Anyone can create a Profilarr-compliant database, and you can connect as many as you like. A default database has already been linked for you, but you're free to remove it and use your own. Press Continue and we'll show you how to link one.", - position: 'right', - completion: { type: 'manual' } + body: "Now let's set up a database. Remember, databases are git repositories containing pre-built custom formats, quality profiles, and more. Anyone can create a Profilarr-compliant database, and you can connect as many as you like. A default database has already been connected for you, but you're free to remove it and use your own. Click Databases to continue.", + position: 'below-right', + completion: { type: 'click' } }, { - id: 'databases-new', - route: '/databases/new', - target: 'db-header', - title: 'Link a Database', - body: 'This is where you link a new database to Profilarr.', - position: 'below', - completion: { type: 'manual' } + id: 'databases-add', + route: '/databases', + target: 'db-add', + title: 'Connect a New Database', + body: 'Click here to start connecting a new database.', + position: 'below-left', + completion: { type: 'click' } }, { id: 'databases-form', + route: '/databases/new', target: 'db-name-repo-branch', title: 'Name, Repository & Branch', - body: 'Start by giving your database a friendly name, then paste in a GitHub repository URL. You can also specify a branch if you need one other than the default. Only GitHub is supported at the moment. If you ever need the same database on a different branch, just link it again with a different branch selected.', + body: 'Start by giving your database a friendly name, then paste in a GitHub repository URL. You can also specify a branch if you need one other than the default. Only GitHub is supported at the moment. If you ever need the same database on a different branch, just connect it again with a different branch selected.', position: 'below', completion: { type: 'manual' } }, @@ -53,6 +54,14 @@ export const databasesStage: Stage = { body: 'This controls how often Profilarr checks the remote repository for new updates, and whether those updates should be pulled in automatically or just trigger a notification so you can review them first.', position: 'above', completion: { type: 'manual' } + }, + { + id: 'databases-save', + target: 'db-save', + title: 'Save', + body: "Once you're happy with your settings, hit Save to connect the database. Profilarr will clone the repository and import all its configurations.", + position: 'below-left', + completion: { type: 'manual' } } ] }; diff --git a/src/lib/client/cutscene/definitions/stages/databases/overview.ts b/src/lib/client/cutscene/definitions/stages/databases/overview.ts new file mode 100644 index 00000000..05bd1730 --- /dev/null +++ b/src/lib/client/cutscene/definitions/stages/databases/overview.ts @@ -0,0 +1,67 @@ +import type { Stage } from '$cutscene/types.ts'; + +export const databaseOverviewStage: Stage = { + id: 'database-manage', + name: 'Overview', + description: 'Tabs and features of a connected database', + prerequisites: [ + { + check: 'hasDatabase', + message: + 'You need at least one connected database to start this stage. Follow the "Link" stage under Databases from the onboarding page to connect one.' + } + ], + steps: [ + { + id: 'db-nav-changes', + route: { resolve: 'firstDatabaseChanges' }, + target: 'db-tab-changes', + title: 'Changes', + body: "The Changes tab is your sync dashboard. It shows the current git status of the database and any incoming upstream updates you haven't pulled yet. From here you can pull updates to stay in sync. If you're a database developer, you'll also see your outgoing draft changes with options to export and commit them back to the repository.", + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'db-nav-commits', + route: { resolve: 'firstDatabaseCommits' }, + target: 'db-tab-commits', + title: 'Commits', + body: 'The Commits tab shows the commit history of the database repository. You can see what changed, when, and by whom.', + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'db-nav-conflicts', + route: { resolve: 'firstDatabaseConflicts' }, + target: 'db-tab-conflicts', + title: 'Conflicts', + body: 'When your local tweaks clash with an upstream update, they surface here. Each conflict shows what changed on both sides. You can resolve them by aligning (dropping your change in favor of upstream) or overriding (keeping your version). The conflict strategy you set on the database controls whether this happens automatically or waits for your input.', + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'db-nav-tweaks', + route: { resolve: 'firstDatabaseTweaks' }, + target: 'db-tab-tweaks', + title: 'Tweaks', + body: "The Tweaks tab is where you'll be able to view and manage your local overrides. This section is still being built.", + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'db-nav-settings', + route: { resolve: 'firstDatabaseSettings' }, + target: 'db-tab-settings', + title: 'Settings', + body: "The Settings tab lets you configure this database's name, sync schedule, auto-pull behavior, and conflict resolution strategy. You can also disconnect the database from here.", + position: 'below', + completion: { type: 'manual' } + }, + { + id: 'db-summary', + title: "You're set up with databases", + body: "You've learned how to connect a database and explored the tabs where you'll manage changes, review commits, resolve conflicts, and configure settings. Your databases will stay in sync automatically if auto-pull is enabled, and any conflicts between your local tweaks and upstream updates will surface in the Conflicts tab when they happen.", + completion: { type: 'manual' } + } + ] +}; diff --git a/src/lib/client/cutscene/definitions/stages/help.ts b/src/lib/client/cutscene/definitions/stages/getting-started/help.ts similarity index 87% rename from src/lib/client/cutscene/definitions/stages/help.ts rename to src/lib/client/cutscene/definitions/stages/getting-started/help.ts index d6d4deb5..569b1093 100644 --- a/src/lib/client/cutscene/definitions/stages/help.ts +++ b/src/lib/client/cutscene/definitions/stages/getting-started/help.ts @@ -1,4 +1,4 @@ -import type { Stage } from '../../types.ts'; +import type { Stage } from '$cutscene/types.ts'; export const helpStage: Stage = { id: 'help', diff --git a/src/lib/client/cutscene/definitions/stages/getting-started/navigation.ts b/src/lib/client/cutscene/definitions/stages/getting-started/navigation.ts new file mode 100644 index 00000000..fb867a46 --- /dev/null +++ b/src/lib/client/cutscene/definitions/stages/getting-started/navigation.ts @@ -0,0 +1,52 @@ +import type { Stage } from '$cutscene/types.ts'; + +export const navigationStage: Stage = { + id: 'navigation', + name: 'Navigation', + description: 'The main sections of the app', + steps: [ + { + id: 'sidebar-overview', + target: 'sidebar', + title: 'Navigation', + body: "Let's take a quick look at the main sections of the app.", + position: 'right', + completion: { type: 'manual' } + }, + { + id: 'nav-databases', + route: '/databases', + target: 'nav-databases', + title: 'Databases', + body: "Databases are where your configurations come from. Connect one and everything inside is ready to browse, customize, and deploy to your Arr instances. We'll walk through connecting one later.", + position: 'right', + completion: { type: 'manual' } + }, + { + id: 'nav-arrs', + route: '/arr', + target: 'nav-arrs', + title: 'Arrs', + body: 'Your Arrs are the Radarr and Sonarr instances that Profilarr manages. Add them here, and any configurations you set up will sync directly to each one. You can connect as many as you need.', + position: 'right', + completion: { type: 'manual' } + }, + { + id: 'nav-config-entities', + target: 'nav-config-entities', + title: 'Configurations', + body: "Databases provide several types of configuration: quality profiles, custom formats, regular expressions, delay profiles, and media management settings like naming and quality definitions. Each has its own section in the sidebar. We'll cover them in more detail later.", + position: 'right', + completion: { type: 'manual' } + }, + { + id: 'nav-settings', + route: '/settings', + target: 'nav-settings', + title: 'Settings', + body: 'This is where you configure Profilarr itself. Scheduled jobs, notification alerts, security, backups, and general preferences all live here. Most defaults work out of the box, so you can come back to this later.', + position: 'right', + completion: { type: 'manual' } + } + ] +}; diff --git a/src/lib/client/cutscene/definitions/stages/personalize.ts b/src/lib/client/cutscene/definitions/stages/getting-started/personalize.ts similarity index 92% rename from src/lib/client/cutscene/definitions/stages/personalize.ts rename to src/lib/client/cutscene/definitions/stages/getting-started/personalize.ts index 314fd5bc..82f1c716 100644 --- a/src/lib/client/cutscene/definitions/stages/personalize.ts +++ b/src/lib/client/cutscene/definitions/stages/getting-started/personalize.ts @@ -1,4 +1,4 @@ -import type { Stage } from '../../types.ts'; +import type { Stage } from '$cutscene/types.ts'; export const personalizeStage: Stage = { id: 'personalize', diff --git a/src/lib/client/cutscene/definitions/stages/getting-started/welcome.ts b/src/lib/client/cutscene/definitions/stages/getting-started/welcome.ts new file mode 100644 index 00000000..8274564d --- /dev/null +++ b/src/lib/client/cutscene/definitions/stages/getting-started/welcome.ts @@ -0,0 +1,29 @@ +import type { Stage } from '$cutscene/types.ts'; + +export const welcomeStage: Stage = { + id: 'welcome', + name: 'Welcome', + description: 'What Profilarr is and how it works', + steps: [ + { + id: 'what-is-profilarr', + title: 'Welcome to Profilarr', + body: 'Profilarr helps you build, test, and deploy media server configurations. Instead of manually configuring Radarr and Sonarr, you connect to curated databases and sync everything across your instances, while keeping any local tweaks you make.', + completion: { type: 'manual' } + }, + { + id: 'nav-onboarding', + target: 'nav-onboarding', + title: 'Learning Profilarr', + body: 'This is the onboarding page. It lives under Settings in the sidebar. Click it to get started.', + position: 'right', + completion: { type: 'click' } + }, + { + id: 'welcome-onboarding', + title: 'The Onboarding Page', + body: 'Each walkthrough here teaches one part of Profilarr with guided, hands-on steps. You can come back here anytime from Settings in the sidebar.', + completion: { type: 'manual' } + } + ] +}; diff --git a/src/lib/client/cutscene/definitions/stages/welcome.ts b/src/lib/client/cutscene/definitions/stages/welcome.ts deleted file mode 100644 index 26881e09..00000000 --- a/src/lib/client/cutscene/definitions/stages/welcome.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { Stage } from '../../types.ts'; - -export const welcomeStage: Stage = { - id: 'welcome', - name: 'Welcome', - description: 'Learn what Profilarr is and how it works', - steps: [ - { - id: 'what-is-profilarr', - title: 'Welcome to Profilarr', - body: 'Profilarr helps you build, test, and deploy media server configurations. Instead of manually configuring Radarr and Sonarr, you connect to curated databases and sync everything across your instances, while keeping any local tweaks you make.', - completion: { type: 'manual' } - }, - { - id: 'sidebar-overview', - target: 'sidebar', - title: 'Navigation', - body: "This is your sidebar. Let's walk through what each section does.", - position: 'right', - completion: { type: 'manual' } - }, - { - id: 'nav-databases', - route: '/databases', - target: 'nav-databases', - title: 'Databases', - body: "Databases provide your configurations. They're curated git repositories full of pre-built profiles, formats, and settings. We'll cover linking one in detail later.", - position: 'right', - completion: { type: 'manual' } - }, - { - id: 'nav-quality-profiles', - route: '/quality-profiles', - target: 'nav-quality-profiles', - title: 'Quality Profiles', - body: 'Quality profiles control how your Arr instances prioritize and score releases. They determine which qualities are acceptable, what order to prefer them in, and how custom formats influence the final ranking.', - position: 'right', - completion: { type: 'manual' } - }, - { - id: 'nav-custom-formats', - route: '/custom-formats', - target: 'nav-custom-formats', - title: 'Custom Formats', - body: 'Custom formats define rules for matching releases by things like resolution, source, codec, and release group. They work together with quality profiles to fine-tune what gets downloaded.', - position: 'right', - completion: { type: 'manual' } - }, - { - id: 'nav-regex', - route: '/regular-expressions', - target: 'nav-regex', - title: 'Regular Expressions', - body: "Unlike Radarr and Sonarr where regex patterns are embedded inside custom formats, Profilarr treats them as their own entity. This means they're reusable across multiple formats. If you use the same pattern in ten formats, you only need to update it in one place.", - position: 'right', - completion: { type: 'manual' } - }, - { - id: 'nav-media-management', - route: '/media-management', - target: 'nav-media-management', - title: 'Media Management', - body: 'Media management covers naming conventions, quality definitions, and media settings. This is where you control how files and folders are named, what quality thresholds are used, and other media-related preferences.', - position: 'right', - completion: { type: 'manual' } - }, - { - id: 'nav-delay-profiles', - route: '/delay-profiles', - target: 'nav-delay-profiles', - title: 'Delay Profiles', - body: 'Delay profiles let you set protocol preferences and release delays. You can prioritize torrents over usenet or vice versa, and set minimum wait times before grabbing a release.', - position: 'right', - completion: { type: 'manual' } - }, - { - id: 'nav-arrs', - route: '/arr', - target: 'nav-arrs', - title: 'Arrs', - body: 'This is where you connect your Radarr and Sonarr instances. Once connected, you can deploy your configurations to them and keep everything in sync.', - position: 'right', - completion: { type: 'manual' } - }, - { - id: 'nav-settings', - route: '/settings', - target: 'nav-settings', - title: 'Settings', - body: 'Jobs, notifications, security, backups, and general app settings all live here.', - position: 'right', - completion: { type: 'manual' } - } - ] -}; diff --git a/src/lib/client/cutscene/prerequisites.ts b/src/lib/client/cutscene/prerequisites.ts new file mode 100644 index 00000000..65ff8d7a --- /dev/null +++ b/src/lib/client/cutscene/prerequisites.ts @@ -0,0 +1,21 @@ +import { STAGES } from './definitions/index.ts'; +import { stateChecks } from './stateChecks.ts'; + +export async function checkPrerequisites( + stageId: string +): Promise<{ ok: true } | { ok: false; message: string }> { + const stage = STAGES[stageId]; + if (!stage?.prerequisites) return { ok: true }; + + for (const prereq of stage.prerequisites) { + const check = stateChecks[prereq.check]; + if (!check) continue; + + const passed = await check(); + if (!passed) { + return { ok: false, message: prereq.message }; + } + } + + return { ok: true }; +} diff --git a/src/lib/client/cutscene/routeResolvers.ts b/src/lib/client/cutscene/routeResolvers.ts new file mode 100644 index 00000000..a0ac89a5 --- /dev/null +++ b/src/lib/client/cutscene/routeResolvers.ts @@ -0,0 +1,62 @@ +/** + * Named route resolver functions for dynamic step routing. + * Each function returns the resolved path string. + * Add new resolvers here as stages require them. + */ +export const routeResolvers: Record Promise> = { + firstDatabaseChanges: async () => { + const res = await fetch('/api/v1/databases'); + const dbs = await res.json(); + return `/databases/${dbs[0].id}/changes`; + }, + firstDatabaseCommits: async () => { + const res = await fetch('/api/v1/databases'); + const dbs = await res.json(); + return `/databases/${dbs[0].id}/commits`; + }, + firstDatabaseConflicts: async () => { + const res = await fetch('/api/v1/databases'); + const dbs = await res.json(); + return `/databases/${dbs[0].id}/conflicts`; + }, + firstDatabaseTweaks: async () => { + const res = await fetch('/api/v1/databases'); + const dbs = await res.json(); + return `/databases/${dbs[0].id}/tweaks`; + }, + firstDatabaseSettings: async () => { + const res = await fetch('/api/v1/databases'); + const dbs = await res.json(); + return `/databases/${dbs[0].id}/settings`; + }, + firstArrLibrary: async () => { + const res = await fetch('/api/v1/arr'); + const arr = await res.json(); + return `/arr/${arr[0].id}/library`; + }, + firstArrSync: async () => { + const res = await fetch('/api/v1/arr'); + const arr = await res.json(); + return `/arr/${arr[0].id}/sync`; + }, + firstArrUpgrades: async () => { + const res = await fetch('/api/v1/arr'); + const arr = await res.json(); + return `/arr/${arr[0].id}/upgrades`; + }, + firstArrRename: async () => { + const res = await fetch('/api/v1/arr'); + const arr = await res.json(); + return `/arr/${arr[0].id}/rename`; + }, + firstArrLogs: async () => { + const res = await fetch('/api/v1/arr'); + const arr = await res.json(); + return `/arr/${arr[0].id}/logs`; + }, + firstArrSettings: async () => { + const res = await fetch('/api/v1/arr'); + const arr = await res.json(); + return `/arr/${arr[0].id}/settings`; + } +}; diff --git a/src/lib/client/cutscene/stateChecks.ts b/src/lib/client/cutscene/stateChecks.ts index bea49edd..a56583c7 100644 --- a/src/lib/client/cutscene/stateChecks.ts +++ b/src/lib/client/cutscene/stateChecks.ts @@ -4,8 +4,16 @@ * Add new checks here as stages require them. */ export const stateChecks: Record Promise> = { - // Future checks will be added here as stages are built, e.g.: - // hasDatabase: async () => { ... }, - // hasArrInstance: async () => { ... }, - // hasSyncedProfile: async () => { ... }, + hasDatabase: async () => { + const res = await fetch('/api/v1/databases'); + if (!res.ok) return false; + const data = await res.json(); + return data.length > 0; + }, + hasArrInstance: async () => { + const res = await fetch('/api/v1/arr'); + if (!res.ok) return false; + const data = await res.json(); + return data.length > 0; + } }; diff --git a/src/lib/client/cutscene/store.ts b/src/lib/client/cutscene/store.ts index 61b69d4c..bb7c1c0e 100644 --- a/src/lib/client/cutscene/store.ts +++ b/src/lib/client/cutscene/store.ts @@ -1,16 +1,16 @@ import { writable, derived } from 'svelte/store'; import { browser } from '$app/environment'; import type { CutsceneState } from './types.ts'; -import { STAGES, PIPELINES } from './definitions/index.ts'; +import { STAGES } from './definitions/index.ts'; +import { checkPrerequisites } from './prerequisites.ts'; +import { alertStore } from '$lib/client/alerts/store'; const STORAGE_KEY = 'cutscene-progress'; const DEFAULT_STATE: CutsceneState = { active: false, - pipelineId: null, stageId: null, stepIndex: 0, - completedStages: [], manualStart: false }; @@ -52,11 +52,6 @@ function createCutsceneStore() { return STAGES[$state.stageId] ?? null; }); - const currentPipeline = derived(state, ($state) => { - if (!$state.pipelineId) return null; - return PIPELINES[$state.pipelineId] ?? null; - }); - function init(shown: boolean): void { onboardingShown.set(shown); if (!shown) { @@ -72,39 +67,39 @@ function createCutsceneStore() { } } - function startStage(stageId: string): void { + async function startStage(stageId: string, manual = true): Promise { const stage = STAGES[stageId]; if (!stage || stage.steps.length === 0) return; + const result = await checkPrerequisites(stageId); + if (!result.ok) { + alertStore.add('error', result.message); + return; + } + justCompleted.set(false); const newState: CutsceneState = { active: true, - pipelineId: null, stageId, stepIndex: 0, - completedStages: [], - manualStart: true + manualStart: manual }; state.set(newState); saveState(newState); } - function startPipeline(pipelineId: string, manual = true): void { - const pipeline = PIPELINES[pipelineId]; - if (!pipeline || pipeline.stages.length === 0) return; + function goBack(): void { + state.update((current) => { + if (!current.active || !current.stageId) return current; - justCompleted.set(false); - const firstStageId = pipeline.stages[0]; - const newState: CutsceneState = { - active: true, - pipelineId, - stageId: firstStageId, - stepIndex: 0, - completedStages: [], - manualStart: manual - }; - state.set(newState); - saveState(newState); + if (current.stepIndex > 0) { + const updated = { ...current, stepIndex: current.stepIndex - 1 }; + saveState(updated); + return updated; + } + + return current; + }); } function advance(): void { @@ -124,42 +119,12 @@ function createCutsceneStore() { } // Stage complete - const completedStages = [...current.completedStages, current.stageId]; - - // If running a pipeline, try next stage - if (current.pipelineId) { - const pipeline = PIPELINES[current.pipelineId]; - if (pipeline) { - const currentStageIndex = pipeline.stages.indexOf(current.stageId); - const nextStageId = pipeline.stages[currentStageIndex + 1]; - - if (nextStageId) { - const updated: CutsceneState = { - ...current, - stageId: nextStageId, - stepIndex: 0, - completedStages - }; - saveState(updated); - return updated; - } - } - } - - // Done (single stage or pipeline complete) clearState(); - const lastStage = current.stageId ? STAGES[current.stageId] : null; + const lastStage = STAGES[current.stageId]; if (current.manualStart && !lastStage?.silent) { justCompleted.set(true); } - return { - active: false, - pipelineId: null, - stageId: null, - stepIndex: 0, - completedStages: [], - manualStart: false - }; + return DEFAULT_STATE; }); } @@ -194,13 +159,12 @@ function createCutsceneStore() { subscribe: state.subscribe, currentStep, currentStage, - currentPipeline, onboardingShown: { subscribe: onboardingShown.subscribe }, justCompleted: { subscribe: justCompleted.subscribe }, init, - startPipeline, startStage, advance, + goBack, cancel, dismiss, dismissCompleted, diff --git a/src/lib/client/cutscene/types.ts b/src/lib/client/cutscene/types.ts index 70296f4f..152ca20b 100644 --- a/src/lib/client/cutscene/types.ts +++ b/src/lib/client/cutscene/types.ts @@ -6,7 +6,7 @@ export type Completion = export interface Step { id: string; - route?: string; + route?: string | { resolve: string }; target?: string; title: string; body: string; @@ -23,16 +23,21 @@ export interface Step { completion: Completion; } +export interface Prerequisite { + check: string; + message: string; +} + export interface Stage { id: string; name: string; description: string; steps: Step[]; silent?: boolean; + prerequisites?: Prerequisite[]; } -export interface Pipeline { - id: string; +export interface StageGroup { name: string; description: string; stages: string[]; @@ -40,9 +45,7 @@ export interface Pipeline { export interface CutsceneState { active: boolean; - pipelineId: string | null; stageId: string | null; stepIndex: number; - completedStages: string[]; manualStart: boolean; } diff --git a/src/lib/client/stores/dirty.ts b/src/lib/client/stores/dirty.ts index 3074ac1c..25b5b60a 100644 --- a/src/lib/client/stores/dirty.ts +++ b/src/lib/client/stores/dirty.ts @@ -65,11 +65,11 @@ export function initEdit(serverData: T) { } /** - * Initialize for create mode - always dirty + * Initialize for create mode - dirty when user changes something */ export function initCreate(defaults: T) { - isNewMode.set(true); - originalSnapshot.set(null); + isNewMode.set(false); + originalSnapshot.set(structuredClone(defaults)); currentData.set(structuredClone(defaults)); } diff --git a/src/lib/client/ui/actions/ActionButton.svelte b/src/lib/client/ui/actions/ActionButton.svelte index f052a5c2..01ea8393 100644 --- a/src/lib/client/ui/actions/ActionButton.svelte +++ b/src/lib/client/ui/actions/ActionButton.svelte @@ -11,6 +11,7 @@ export let title: string = ''; export let type: 'button' | 'submit' = 'button'; export let variant: 'neutral' | 'danger' = 'neutral'; + export let onboarding: string | undefined = undefined; let isHovered = false; let leaveTimer: ReturnType | null = null; @@ -46,6 +47,7 @@ on:mouseenter={handleMouseEnter} on:mouseleave={handleMouseLeave} role="group" + data-onboarding={onboarding} >
- -
- {#if latestAlert} - - {/if} -
-
diff --git a/src/lib/client/ui/navigation/pageNav/group.svelte b/src/lib/client/ui/navigation/pageNav/group.svelte index 8b268276..aa11ed2c 100644 --- a/src/lib/client/ui/navigation/pageNav/group.svelte +++ b/src/lib/client/ui/navigation/pageNav/group.svelte @@ -10,7 +10,6 @@ export let initialOpen: boolean = true; export let hasItems: boolean = false; export let onboardingId: string | undefined = undefined; - let isOpen = initialOpen; function toggleOpen() { @@ -18,8 +17,10 @@ } -
- +
+
+ +
{#if isOpen && hasItems}
diff --git a/src/lib/client/ui/navigation/pageNav/groupItem.svelte b/src/lib/client/ui/navigation/pageNav/groupItem.svelte index 87812094..ddf6cc17 100644 --- a/src/lib/client/ui/navigation/pageNav/groupItem.svelte +++ b/src/lib/client/ui/navigation/pageNav/groupItem.svelte @@ -13,9 +13,11 @@ iconSrc?: string; /** Optional click handler (use e.preventDefault() to override navigation) */ onclick?: (e: MouseEvent) => void; + /** Optional data-onboarding attribute for cutscene targeting */ + onboardingId?: string; } - let { label, href, activePattern, icon, iconSrc, onclick }: Props = $props(); + let { label, href, activePattern, icon, iconSrc, onclick, onboardingId }: Props = $props(); const isActive = $derived.by(() => { const pathname = $page.url.pathname; @@ -36,6 +38,7 @@ - - {#if parserAvailable} - - {/if} - +
+ + {#if parserAvailable} + + {/if} + - - - - - - - - - - + + + + + + + + + +
+ handleTabSelect(tab.href)} + data-onboarding={tab.onboarding || null} class="flex cursor-pointer items-center gap-2 border-b-2 px-4 py-3 text-sm font-medium transition-colors {tab.active ? 'border-accent-600 text-accent-600 dark:border-accent-500 dark:text-accent-500' : 'border-transparent text-neutral-600 hover:border-neutral-300 hover:text-neutral-900 dark:text-neutral-400 dark:hover:border-neutral-700 dark:hover:text-neutral-50'}" diff --git a/src/lib/client/ui/state/EmptyState.svelte b/src/lib/client/ui/state/EmptyState.svelte index 6561bffc..c00d3848 100644 --- a/src/lib/client/ui/state/EmptyState.svelte +++ b/src/lib/client/ui/state/EmptyState.svelte @@ -1,5 +1,6 @@
diff --git a/src/lib/shared/features.ts b/src/lib/shared/features.ts index f43bbf30..b0507dc2 100644 --- a/src/lib/shared/features.ts +++ b/src/lib/shared/features.ts @@ -9,5 +9,5 @@ export const FEATURES = { /** AI-powered commit message generation */ ai: false, /** Cutscene onboarding system */ - cutscene: false + cutscene: true } as const; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index c56c6d34..6a66b229 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -7,7 +7,7 @@ import AlertContainer from '$alerts/AlertContainer.svelte'; import HelpButton from '$ui/help/HelpButton.svelte'; import CutsceneOverlay from '$lib/client/cutscene/CutsceneOverlay.svelte'; - import CutscenePrompt from '$lib/client/cutscene/CutscenePrompt.svelte'; + import CutsceneComplete from '$lib/client/cutscene/CutsceneComplete.svelte'; import { cutscene } from '$lib/client/cutscene/store'; import { FEATURES } from '$lib/shared/features'; @@ -55,7 +55,6 @@ {#if cutsceneEnabled && isDesktop} - {/if} {/if} diff --git a/src/routes/arr/+page.svelte b/src/routes/arr/+page.svelte index 0bed3497..b563d1b0 100644 --- a/src/routes/arr/+page.svelte +++ b/src/routes/arr/+page.svelte @@ -52,13 +52,19 @@ buttonText="Add Instance" buttonHref="/arr/new" buttonIcon={Plus} + onboarding="arr-add" /> {:else}
- goto('/arr/new')} /> + goto('/arr/new')} + onboarding="arr-add" + /> (showInfoModal = true)} /> diff --git a/src/routes/arr/[id]/+layout.svelte b/src/routes/arr/[id]/+layout.svelte index a5d5c927..12e0fad0 100644 --- a/src/routes/arr/[id]/+layout.svelte +++ b/src/routes/arr/[id]/+layout.svelte @@ -13,14 +13,16 @@ label: 'Library', href: `/arr/${instanceId}/library`, active: currentPath.includes('/library'), - icon: Library + icon: Library, + onboarding: 'arr-tab-library' }; $: syncTab = { label: 'Sync', href: `/arr/${instanceId}/sync`, active: currentPath.includes('/sync'), - icon: RefreshCw + icon: RefreshCw, + onboarding: 'arr-tab-sync' }; $: otherTabs = [ @@ -28,25 +30,29 @@ label: 'Upgrades', href: `/arr/${instanceId}/upgrades`, active: currentPath.includes('/upgrades'), - icon: ArrowUpCircle + icon: ArrowUpCircle, + onboarding: 'arr-tab-upgrades' }, { label: 'Renames', href: `/arr/${instanceId}/rename`, active: currentPath.includes('/rename'), - icon: FileEdit + icon: FileEdit, + onboarding: 'arr-tab-renames' }, { label: 'Logs', href: `/arr/${instanceId}/logs`, active: currentPath.includes('/logs'), - icon: ScrollText + icon: ScrollText, + onboarding: 'arr-tab-logs' }, { label: 'Settings', href: `/arr/${instanceId}/settings`, active: currentPath.includes('/settings'), - icon: Settings + icon: Settings, + onboarding: 'arr-tab-settings' } ]; diff --git a/src/routes/arr/[id]/rename/components/RenameSettings.svelte b/src/routes/arr/[id]/rename/components/RenameSettings.svelte index f5d09c86..b7b5f0f6 100644 --- a/src/routes/arr/[id]/rename/components/RenameSettings.svelte +++ b/src/routes/arr/[id]/rename/components/RenameSettings.svelte @@ -108,7 +108,7 @@
-
+
Folders @@ -126,7 +126,7 @@
-
+
Summary @@ -147,7 +147,7 @@ -
+
Schedule @@ -160,7 +160,7 @@
-
+
Ignore Tag diff --git a/src/routes/arr/[id]/sync/+page.svelte b/src/routes/arr/[id]/sync/+page.svelte index 52e22fb2..61e3d351 100644 --- a/src/routes/arr/[id]/sync/+page.svelte +++ b/src/routes/arr/[id]/sync/+page.svelte @@ -130,7 +130,9 @@

-
@@ -313,7 +315,7 @@ class="space-y-4 rounded-lg border border-neutral-200 bg-white p-4 dark:border-neutral-800 dark:bg-neutral-900" > -
+
Type{#if mode === 'create'}*{/if} @@ -330,63 +332,68 @@ on:change={(e) => update('type', e.detail)} />
- -
-
- update('name', e.detail)} - /> + +
+ +
+
+ update('name', e.detail)} + /> +
+
+ Status + update('enabled', e.detail)} + /> +
-
- Status - update('enabled', e.detail)} - /> -
-
- {#if enabled === 'false'} -

- Disabled instances are excluded from sync operations -

- {/if} - - update('url', e.detail)} - /> - -
-
- update('apiKey', e.detail)} - /> -
-
diff --git a/src/routes/databases/+page.svelte b/src/routes/databases/+page.svelte index 3cb10010..3ed979a7 100644 --- a/src/routes/databases/+page.svelte +++ b/src/routes/databases/+page.svelte @@ -53,13 +53,19 @@ buttonText="Link Database" buttonHref="/databases/new" buttonIcon={Plus} + onboarding="db-add" /> {:else}
- goto('/databases/new')} /> + goto('/databases/new')} + onboarding="db-add" + /> (showInfoModal = true)} /> diff --git a/src/routes/databases/[id]/+layout.svelte b/src/routes/databases/[id]/+layout.svelte index f00f97d7..19a8ce33 100644 --- a/src/routes/databases/[id]/+layout.svelte +++ b/src/routes/databases/[id]/+layout.svelte @@ -19,25 +19,29 @@ label: 'Changes', href: `/databases/${database.id}/changes`, icon: GitBranch, - active: currentPath.endsWith('/changes') + active: currentPath.endsWith('/changes'), + onboarding: 'db-tab-changes' }, { label: 'Commits', href: `/databases/${database.id}/commits`, icon: History, - active: currentPath.includes('/commits') + active: currentPath.includes('/commits'), + onboarding: 'db-tab-commits' }, { label: 'Conflicts', href: `/databases/${database.id}/conflicts`, icon: GitPullRequestClosed, - active: currentPath.includes('/conflicts') + active: currentPath.includes('/conflicts'), + onboarding: 'db-tab-conflicts' }, { label: 'Tweaks', href: `/databases/${database.id}/tweaks`, icon: Wrench, - active: currentPath.includes('/tweaks') + active: currentPath.includes('/tweaks'), + onboarding: 'db-tab-tweaks' }, ...(database.hasPat ? [ @@ -53,7 +57,8 @@ label: 'Settings', href: `/databases/${database.id}/settings`, icon: Settings, - active: currentPath.includes('/settings') + active: currentPath.includes('/settings'), + onboarding: 'db-tab-settings' } ] : []; diff --git a/src/routes/databases/components/InstanceForm.svelte b/src/routes/databases/components/InstanceForm.svelte index f7326fb8..051780d3 100644 --- a/src/routes/databases/components/InstanceForm.svelte +++ b/src/routes/databases/components/InstanceForm.svelte @@ -241,13 +241,15 @@ on:click={() => (showDeleteModal = true)} /> {/if} -
{#if mode === 'edit'}
- + {/if} {/each} - - {/if} -
- {/if} - - - {#if stageResults.length > 0} -
-

- Stages -

- {#if $view === 'table'} -
- - - - - - - - - - - {#each stageResults as item} - - - - - - - {/each} - -
NameDescriptionSteps
- {item.name} - - {item.description} - - {item.count} - -
- {:else} - - {#each stageResults as item} - -
-
-
-

- {item.name} -

- -
-

- {item.description} -

-
-
-
- {/each} -
- {/if} -
- {/if} + + {/each} +
diff --git a/src/routes/settings/+page.svelte b/src/routes/settings/+page.svelte index 8d08fa49..32e80dd5 100644 --- a/src/routes/settings/+page.svelte +++ b/src/routes/settings/+page.svelte @@ -1,5 +1,14 @@