@futo-org/backups-orchestrator-ui 0.27.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (163) hide show
  1. package/dist/components/backends/BackendItem.svelte +28 -24
  2. package/dist/components/backends/BackendsList.svelte +33 -34
  3. package/dist/components/backends/BackendsList.svelte.d.ts +1 -0
  4. package/dist/components/backends/dialogs/SelectBackendModal.svelte +44 -55
  5. package/dist/components/backups/BackupItem.svelte +33 -26
  6. package/dist/components/backups/BackupStatus.svelte +200 -0
  7. package/dist/components/backups/BackupStatus.svelte.d.ts +19 -0
  8. package/dist/components/backups/BackupsList.svelte +17 -11
  9. package/dist/components/backups/dialogs/RestoreSnapshotModal.svelte +2 -2
  10. package/dist/components/backups/dialogs/SelectRepositoryModal.svelte +26 -36
  11. package/dist/components/backups/dialogs/ViewStatusModal.svelte +175 -0
  12. package/dist/components/backups/dialogs/ViewStatusModal.svelte.d.ts +8 -0
  13. package/dist/components/backups/run-history/RepositoryRunHistory.svelte +23 -12
  14. package/dist/components/backups/run-history/RepositoryRunHistory.svelte.d.ts +2 -0
  15. package/dist/components/backups/run-history/RepositoryRunHistoryItem.svelte +46 -20
  16. package/dist/components/backups/run-history/RepositoryRunHistoryPage.svelte +136 -0
  17. package/dist/components/backups/run-history/RepositoryRunHistoryPage.svelte.d.ts +8 -0
  18. package/dist/components/backups/run-history/RunHistoryModal.svelte +2 -2
  19. package/dist/components/backups/snapshots-list/RepositorySnapshotsList.svelte +22 -10
  20. package/dist/components/backups/snapshots-list/RepositorySnapshotsList.svelte.d.ts +2 -0
  21. package/dist/components/backups/snapshots-list/RepositorySnapshotsListItem.svelte +23 -21
  22. package/dist/components/backups/snapshots-list/RepositorySnapshotsPage.svelte +115 -0
  23. package/dist/components/backups/snapshots-list/RepositorySnapshotsPage.svelte.d.ts +9 -0
  24. package/dist/components/backups/snapshots-list/SnapshotsListModal.svelte +2 -2
  25. package/dist/components/dashboard/Dashboard.svelte +14 -7
  26. package/dist/components/dashboard/Dashboard.svelte.d.ts +1 -1
  27. package/dist/components/dashboard/DashboardAvgBackupTime.svelte +1 -1
  28. package/dist/components/dashboard/DashboardBackupHealth.svelte +5 -9
  29. package/dist/components/dashboard/DashboardBackupHealth.svelte.d.ts +1 -1
  30. package/dist/components/dashboard/DashboardCurrentUsage.svelte +1 -1
  31. package/dist/components/dashboard/DashboardDailyBackupTime.svelte +1 -1
  32. package/dist/components/dashboard/DashboardInstall.svelte +1 -1
  33. package/dist/components/dashboard/DashboardRecentBackups.svelte +47 -78
  34. package/dist/components/dashboard/DashboardTotalStored.svelte +1 -1
  35. package/dist/components/integrations/immich/ImmichBackupSettings.svelte +12 -0
  36. package/dist/components/integrations/immich/ImmichBackupSettings.svelte.d.ts +18 -0
  37. package/dist/components/integrations/immich/ImmichBackupsCard.svelte +109 -0
  38. package/dist/components/integrations/immich/ImmichBackupsCard.svelte.d.ts +13 -0
  39. package/dist/components/integrations/immich/ImmichBackupsNavButton.svelte +55 -0
  40. package/dist/components/integrations/immich/ImmichBackupsNavButton.svelte.d.ts +10 -0
  41. package/dist/components/integrations/immich/ImmichBackupsPage.svelte +16 -3
  42. package/dist/components/integrations/immich/ImmichBackupsPage.svelte.d.ts +8 -1
  43. package/dist/components/integrations/immich/ImmichBackupsSidebarItem.svelte +102 -0
  44. package/dist/components/integrations/immich/ImmichBackupsSidebarItem.svelte.d.ts +8 -0
  45. package/dist/components/integrations/immich/ImmichManageBackup.svelte +53 -14
  46. package/dist/components/integrations/immich/ImmichManageBackupOverview.svelte +45 -71
  47. package/dist/components/integrations/immich/ImmichOnboardingSetupFlow.svelte +123 -82
  48. package/dist/components/integrations/immich/ImmichOnboardingSetupFlow.svelte.d.ts +1 -1
  49. package/dist/components/integrations/immich/settings/ImmichSettingsBackupContents.svelte +313 -0
  50. package/dist/components/integrations/immich/settings/ImmichSettingsBackupContents.svelte.d.ts +3 -0
  51. package/dist/components/integrations/immich/settings/ImmichSettingsSchedule.svelte +218 -0
  52. package/dist/components/integrations/immich/settings/ImmichSettingsSchedule.svelte.d.ts +3 -0
  53. package/dist/components/integrations/immich/settings/ImmichSettingsStorage.svelte +207 -0
  54. package/dist/components/integrations/immich/settings/ImmichSettingsStorage.svelte.d.ts +3 -0
  55. package/dist/components/onboarding/RecoveryKeyDisplay.svelte +34 -20
  56. package/dist/components/onboarding/SampleOnboarding.svelte +57 -38
  57. package/dist/components/onboarding/stages/OnboardingStageBackupServices.svelte +10 -24
  58. package/dist/components/onboarding/steps/OnboardingStep1FinishSetup.svelte +44 -0
  59. package/dist/components/onboarding/steps/OnboardingStep1FinishSetup.svelte.d.ts +7 -0
  60. package/dist/components/onboarding/steps/OnboardingStep2ConnectAccount.svelte +50 -0
  61. package/dist/components/onboarding/steps/OnboardingStep2ConnectAccount.svelte.d.ts +7 -0
  62. package/dist/components/onboarding/steps/OnboardingStep3SaveRecoveryKey.svelte +45 -0
  63. package/dist/components/onboarding/steps/OnboardingStep3SaveRecoveryKey.svelte.d.ts +8 -0
  64. package/dist/components/onboarding/steps/OnboardingStep4FirstBackup.svelte +67 -0
  65. package/dist/components/onboarding/steps/OnboardingStep4FirstBackup.svelte.d.ts +9 -0
  66. package/dist/components/onboarding/steps/OnboardingStepLayout.svelte +58 -0
  67. package/dist/components/onboarding/steps/OnboardingStepLayout.svelte.d.ts +19 -0
  68. package/dist/components/onboarding/upsell/UpsellBackupFeaturesGrid.svelte +52 -0
  69. package/dist/components/onboarding/upsell/UpsellBackupFeaturesGrid.svelte.d.ts +18 -0
  70. package/dist/components/onboarding/upsell/UpsellExplainFutoBackups.svelte +33 -0
  71. package/dist/components/onboarding/upsell/UpsellExplainFutoBackups.svelte.d.ts +8 -0
  72. package/dist/components/onboarding/upsell/UpsellFrequentlyAskedQuestions.svelte +32 -0
  73. package/dist/components/onboarding/upsell/UpsellFrequentlyAskedQuestions.svelte.d.ts +11 -0
  74. package/dist/components/onboarding/upsell/UpsellFutoBackupsBadge.svelte +7 -0
  75. package/dist/components/onboarding/upsell/UpsellFutoBackupsBadge.svelte.d.ts +18 -0
  76. package/dist/components/onboarding/upsell/UpsellHowItWorks.svelte +46 -0
  77. package/dist/components/onboarding/upsell/UpsellHowItWorks.svelte.d.ts +6 -0
  78. package/dist/components/onboarding/upsell/UpsellModal.svelte +43 -0
  79. package/dist/components/onboarding/upsell/UpsellModal.svelte.d.ts +9 -0
  80. package/dist/components/onboarding/upsell/UpsellPage.svelte +40 -0
  81. package/dist/components/onboarding/upsell/UpsellPage.svelte.d.ts +14 -0
  82. package/dist/components/onboarding/upsell/UpsellProtectYourImmichLibrary.svelte +58 -0
  83. package/dist/components/onboarding/upsell/UpsellProtectYourImmichLibrary.svelte.d.ts +8 -0
  84. package/dist/components/schedules/RepositoryPicker.svelte +45 -38
  85. package/dist/components/schedules/ScheduleItem.svelte +32 -26
  86. package/dist/components/schedules/ScheduleList.svelte +16 -10
  87. package/dist/components/settings/GlobalSettings.svelte +8 -0
  88. package/dist/components/settings/GlobalSettings.svelte.d.ts +18 -0
  89. package/dist/components/settings/SettingsBackends.svelte +16 -0
  90. package/dist/components/settings/SettingsBackends.svelte.d.ts +18 -0
  91. package/dist/components/settings/SettingsBilling.svelte +69 -0
  92. package/dist/components/settings/SettingsBilling.svelte.d.ts +6 -0
  93. package/dist/components/settings/SettingsNotifications.svelte +47 -0
  94. package/dist/components/settings/SettingsNotifications.svelte.d.ts +6 -0
  95. package/dist/components/test/ImmichTestUi.svelte +143 -27
  96. package/dist/components/test/TestUi.svelte +4 -4
  97. package/dist/components/test/dashboard/ActiveJobs.svelte +2 -2
  98. package/dist/components/test/immich/MockImmichHideBackupsReminder.svelte +12 -0
  99. package/dist/components/test/immich/MockImmichHideBackupsReminder.svelte.d.ts +3 -0
  100. package/dist/components/test/immich/MockImmichPhotos.svelte +36 -0
  101. package/dist/components/test/immich/MockImmichPhotos.svelte.d.ts +18 -0
  102. package/dist/components/test/immich/MockImmichPurchaseInfo.svelte +29 -0
  103. package/dist/components/test/immich/MockImmichPurchaseInfo.svelte.d.ts +18 -0
  104. package/dist/components/test/immich/MockImmichServerStatus.svelte +9 -0
  105. package/dist/components/test/immich/MockImmichServerStatus.svelte.d.ts +18 -0
  106. package/dist/components/test/immich/MockImmichSidebar.svelte +20 -0
  107. package/dist/components/test/immich/MockImmichSidebar.svelte.d.ts +7 -0
  108. package/dist/components/test/immich/MockImmichStorageSpace.svelte +25 -0
  109. package/dist/components/test/immich/MockImmichStorageSpace.svelte.d.ts +7 -0
  110. package/dist/components/ui/Accordion.svelte +67 -0
  111. package/dist/components/ui/Accordion.svelte.d.ts +11 -0
  112. package/dist/components/ui/ListToolbar.svelte +60 -0
  113. package/dist/components/ui/ListToolbar.svelte.d.ts +16 -0
  114. package/dist/components/ui/PageLayout.svelte +21 -9
  115. package/dist/components/ui/PageLayout.svelte.d.ts +1 -0
  116. package/dist/components/ui/Pagination.svelte +80 -0
  117. package/dist/components/ui/Pagination.svelte.d.ts +8 -0
  118. package/dist/components/ui/PathListField.svelte +17 -14
  119. package/dist/components/ui/StackList.svelte +18 -18
  120. package/dist/components/ui/StackList.svelte.d.ts +6 -27
  121. package/dist/components/ui/StackListItem.svelte +47 -25
  122. package/dist/components/ui/StackListItem.svelte.d.ts +5 -2
  123. package/dist/components/ui/StackListOption.svelte +33 -0
  124. package/dist/components/ui/StackListOption.svelte.d.ts +11 -0
  125. package/dist/components/ui/StackListPlaceholder.svelte +12 -0
  126. package/dist/components/ui/StackListPlaceholder.svelte.d.ts +7 -0
  127. package/dist/components/ui/StepNumber.svelte +20 -0
  128. package/dist/components/ui/StepNumber.svelte.d.ts +7 -0
  129. package/dist/components/util/Suspense.svelte +7 -3
  130. package/dist/components/util/YuccaContext.svelte +1 -1
  131. package/dist/events.js +1 -1
  132. package/dist/fetch-client.d.ts +2 -0
  133. package/dist/index.d.ts +6 -1
  134. package/dist/index.js +6 -1
  135. package/dist/services/backend.service.js +3 -0
  136. package/dist/services/immich.integration.service.d.ts +1 -1
  137. package/dist/services/immich.integration.service.js +2 -3
  138. package/dist/services/integrations.service.d.ts +1 -0
  139. package/dist/services/integrations.service.js +2 -1
  140. package/dist/services/log.service.svelte.d.ts +12 -0
  141. package/dist/services/log.service.svelte.js +15 -1
  142. package/dist/services/repository.service.js +9 -3
  143. package/dist/services/runHistory.service.d.ts +1 -1
  144. package/dist/services/runHistory.service.js +5 -4
  145. package/dist/utils/format.d.ts +1 -1
  146. package/dist/utils/format.js +2 -2
  147. package/package.json +3 -3
  148. package/dist/components/backups/dialogs/ViewLogModal.svelte +0 -208
  149. package/dist/components/backups/dialogs/ViewLogModal.svelte.d.ts +0 -7
  150. package/dist/components/integrations/immich/ImmichConfigureBackup.svelte +0 -403
  151. package/dist/components/integrations/immich/ImmichConfigureBackup.svelte.d.ts +0 -9
  152. package/dist/components/integrations/immich/ImmichConfirmDefaultBackup.svelte +0 -80
  153. package/dist/components/integrations/immich/ImmichConfirmDefaultBackup.svelte.d.ts +0 -8
  154. package/dist/components/onboarding/stages/OnboardingStageKeyConfirm.svelte +0 -56
  155. package/dist/components/onboarding/stages/OnboardingStageKeyConfirm.svelte.d.ts +0 -9
  156. package/dist/components/onboarding/stages/OnboardingStageKeyIntro.svelte +0 -50
  157. package/dist/components/onboarding/stages/OnboardingStageKeyIntro.svelte.d.ts +0 -7
  158. package/dist/components/onboarding/stages/OnboardingStageKeySave.svelte +0 -44
  159. package/dist/components/onboarding/stages/OnboardingStageKeySave.svelte.d.ts +0 -8
  160. package/dist/components/onboarding/stages/OnboardingStageWelcome.svelte +0 -56
  161. package/dist/components/onboarding/stages/OnboardingStageWelcome.svelte.d.ts +0 -9
  162. package/dist/components/util/TimedButton.svelte +0 -37
  163. package/dist/components/util/TimedButton.svelte.d.ts +0 -7
@@ -0,0 +1,218 @@
1
+ <script lang="ts">
2
+ import Accordion from "../../../ui/Accordion.svelte";
3
+ import OnEvents from "../../../util/OnEvents.svelte";
4
+ import {
5
+ useConfigureImmichIntegration,
6
+ useIntegrationEventHandler,
7
+ useIntegrations,
8
+ } from "../../../../services/integrations.service";
9
+ import {
10
+ useRepositories,
11
+ useRepositoryEventHandler,
12
+ } from "../../../../services/repository.service";
13
+ import {
14
+ useScheduleEventHandler,
15
+ useSchedules,
16
+ } from "../../../../services/schedule.service";
17
+ import {
18
+ Button,
19
+ Field,
20
+ HStack,
21
+ LoadingSpinner,
22
+ Select,
23
+ Stack,
24
+ Switch,
25
+ Text,
26
+ } from "@immich/ui";
27
+ import { mdiClockOutline } from "@mdi/js";
28
+ import cronstrue from "cronstrue";
29
+
30
+ type Frequency = "daily" | "weekly" | "monthly";
31
+
32
+ const frequencies = [
33
+ { value: "daily", label: "Daily" },
34
+ { value: "weekly", label: "Weekly" },
35
+ { value: "monthly", label: "Monthly" },
36
+ ];
37
+
38
+ const startTimes = Array.from({ length: 24 }, (_, hour) => ({
39
+ value: String(hour),
40
+ label: `${String(hour).padStart(2, "0")}:00`,
41
+ }));
42
+
43
+ const integrations = useIntegrations();
44
+ const repositories = useRepositories();
45
+ const schedules = useSchedules();
46
+ const mutation = useConfigureImmichIntegration();
47
+
48
+ const { onIntegrationUpdate } = useIntegrationEventHandler();
49
+ const { onRepositoryCreate, onRepositoryUpdate, onRepositoryDelete } =
50
+ useRepositoryEventHandler();
51
+ const { onScheduleCreate, onScheduleUpdate, onScheduleDelete } =
52
+ useScheduleEventHandler();
53
+
54
+ const integration = $derived(integrations.data?.immichIntegration);
55
+
56
+ const repository = $derived(
57
+ integration
58
+ ? repositories.data?.find((entry) => entry.id === integration.id)
59
+ : undefined,
60
+ );
61
+
62
+ const schedule = $derived(
63
+ integration
64
+ ? schedules.data?.find((entry) => entry.id === integration.scheduleId)
65
+ : undefined,
66
+ );
67
+
68
+ const isLoading = $derived(
69
+ integrations.isLoading || repositories.isLoading || schedules.isLoading,
70
+ );
71
+
72
+ let frequency = $state<Frequency>("daily");
73
+ let startTime = $state("3");
74
+ let enabled = $state(true);
75
+ let loaded = false;
76
+
77
+ const parse = (cron: string) => {
78
+ // notes
79
+ // cron rewritten on write
80
+ // - minute discarded
81
+ // - weekly/monthly forced to set date
82
+ // => need server-determined date for load distribution
83
+ const [minute, hour, day, , weekday] = cron.split(" ");
84
+
85
+ return {
86
+ hour: /^\d+$/.test(hour ?? "") ? hour : "3",
87
+ minute: /^\d+$/.test(minute ?? "") ? Number(minute) : 0,
88
+ frequency: (day !== "*"
89
+ ? "monthly"
90
+ : weekday !== "*"
91
+ ? "weekly"
92
+ : "daily") as Frequency,
93
+ };
94
+ };
95
+
96
+ const load = () => {
97
+ if (!schedule) {
98
+ return;
99
+ }
100
+
101
+ const parsed = parse(schedule.cron);
102
+ frequency = parsed.frequency;
103
+ startTime = parsed.hour;
104
+ enabled = !schedule.paused;
105
+ };
106
+
107
+ $effect(() => {
108
+ if (loaded || !schedule) {
109
+ return;
110
+ }
111
+
112
+ loaded = true;
113
+ load();
114
+ });
115
+
116
+ const cron = $derived.by(() => {
117
+ const hour = Number(startTime);
118
+
119
+ switch (frequency) {
120
+ case "weekly": {
121
+ return `0 ${hour} * * 0`;
122
+ }
123
+ case "monthly": {
124
+ return `0 ${hour} 1 * *`;
125
+ }
126
+ default: {
127
+ return `0 ${hour} * * *`;
128
+ }
129
+ }
130
+ });
131
+
132
+ const onSave = () => {
133
+ if (!integration || !repository || !schedule) {
134
+ return;
135
+ }
136
+
137
+ mutation.mutate({
138
+ name: repository.name,
139
+ worm: repository.worm,
140
+ dataFolders: integration.configuration.dataFolders,
141
+ backupConfiguration: integration.configuration.backupConfiguration,
142
+ libraries: integration.configuration.libraries,
143
+ retentionPolicy: repository.configuration?.retentionPolicy ?? null,
144
+ cron,
145
+ paused: !enabled,
146
+ });
147
+ };
148
+ </script>
149
+
150
+ <OnEvents
151
+ {onIntegrationUpdate}
152
+ {onRepositoryCreate}
153
+ {onRepositoryUpdate}
154
+ {onRepositoryDelete}
155
+ {onScheduleCreate}
156
+ {onScheduleUpdate}
157
+ {onScheduleDelete}
158
+ />
159
+
160
+ <Accordion
161
+ title="Schedule"
162
+ subtitle="Manage when backups run."
163
+ icon={mdiClockOutline}
164
+ >
165
+ <Stack gap={4} class="pt-2">
166
+ {#if isLoading}
167
+ <LoadingSpinner />
168
+ {:else}
169
+ <Field
170
+ label="Run backups automatically"
171
+ description="Back up your library on a recurring schedule."
172
+ color="primary"
173
+ >
174
+ <Switch bind:checked={enabled} />
175
+ </Field>
176
+
177
+ <Field
178
+ label="Frequency"
179
+ description="How often backups should run."
180
+ color="primary"
181
+ required="indicator"
182
+ disabled={!enabled}
183
+ >
184
+ <Select options={frequencies} bind:value={frequency} />
185
+ </Field>
186
+
187
+ <Field
188
+ label="Start time"
189
+ description="When the scheduled backup starts."
190
+ color="primary"
191
+ required="indicator"
192
+ disabled={!enabled}
193
+ >
194
+ <Select options={startTimes} bind:value={startTime} />
195
+ </Field>
196
+
197
+ <Text size="small" color="muted">
198
+ Backups will run <span class="lowercase"
199
+ >{cronstrue.toString(cron, { verbose: true })}</span
200
+ >.
201
+ </Text>
202
+
203
+ <HStack gap={2} class="justify-end">
204
+ <Button
205
+ shape="round"
206
+ color="secondary"
207
+ disabled={mutation.isPending}
208
+ onclick={load}
209
+ >
210
+ Reset
211
+ </Button>
212
+ <Button shape="round" loading={mutation.isPending} onclick={onSave}>
213
+ Save
214
+ </Button>
215
+ </HStack>
216
+ {/if}
217
+ </Stack>
218
+ </Accordion>
@@ -0,0 +1,3 @@
1
+ declare const ImmichSettingsSchedule: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ImmichSettingsSchedule = ReturnType<typeof ImmichSettingsSchedule>;
3
+ export default ImmichSettingsSchedule;
@@ -0,0 +1,207 @@
1
+ <script lang="ts">
2
+ import BackendsList from "../../../backends/BackendsList.svelte";
3
+ import Accordion from "../../../ui/Accordion.svelte";
4
+ import OnEvents from "../../../util/OnEvents.svelte";
5
+ import type { RetentionPolicyDto } from "../../../../fetch-client";
6
+ import {
7
+ useConfigureImmichIntegration,
8
+ useIntegrationEventHandler,
9
+ useIntegrations,
10
+ } from "../../../../services/integrations.service";
11
+ import {
12
+ useRepositories,
13
+ useRepositoryEventHandler,
14
+ } from "../../../../services/repository.service";
15
+ import {
16
+ useScheduleEventHandler,
17
+ useSchedules,
18
+ } from "../../../../services/schedule.service";
19
+ import {
20
+ Button,
21
+ Field,
22
+ HStack,
23
+ Input,
24
+ LoadingSpinner,
25
+ Select,
26
+ Stack,
27
+ Switch,
28
+ } from "@immich/ui";
29
+ import { mdiDownloadBox } from "@mdi/js";
30
+
31
+ type RetentionChoice = {
32
+ key: string;
33
+ label: string;
34
+ policy: RetentionPolicyDto | null;
35
+ };
36
+
37
+ const retentionChoices: RetentionChoice[] = [
38
+ { key: "15d", label: "After 15 days", policy: { keepWithin: "15d" } },
39
+ { key: "30d", label: "After 30 days", policy: { keepWithin: "30d" } },
40
+ { key: "60d", label: "After 60 days", policy: { keepWithin: "60d" } },
41
+ { key: "90d", label: "After 90 days", policy: { keepWithin: "90d" } },
42
+ { key: "2", label: "Keep latest two backups", policy: { keepLast: 2 } },
43
+ { key: "never", label: "Never (keep all backups)", policy: null },
44
+ ];
45
+
46
+ const defaultRetentionKey = "60d";
47
+
48
+ const integrations = useIntegrations();
49
+ const repositories = useRepositories();
50
+ const schedules = useSchedules();
51
+ const mutation = useConfigureImmichIntegration();
52
+
53
+ const { onIntegrationUpdate } = useIntegrationEventHandler();
54
+ const { onRepositoryCreate, onRepositoryUpdate, onRepositoryDelete } =
55
+ useRepositoryEventHandler();
56
+ const { onScheduleCreate, onScheduleUpdate, onScheduleDelete } =
57
+ useScheduleEventHandler();
58
+
59
+ const integration = $derived(integrations.data?.immichIntegration);
60
+
61
+ const repository = $derived(
62
+ integration
63
+ ? repositories.data?.find((entry) => entry.id === integration.id)
64
+ : undefined,
65
+ );
66
+
67
+ const schedule = $derived(
68
+ integration
69
+ ? schedules.data?.find((entry) => entry.id === integration.scheduleId)
70
+ : undefined,
71
+ );
72
+
73
+ const isLoading = $derived(
74
+ integrations.isLoading || repositories.isLoading || schedules.isLoading,
75
+ );
76
+
77
+ let name = $state("");
78
+ let worm = $state(false);
79
+ let retentionKey = $state(defaultRetentionKey);
80
+ let loaded = false;
81
+
82
+ const load = () => {
83
+ if (!repository) {
84
+ return;
85
+ }
86
+
87
+ name = repository.name;
88
+ worm = repository.worm;
89
+
90
+ const policy = repository.configuration?.retentionPolicy ?? null;
91
+ retentionKey =
92
+ retentionChoices.find(
93
+ (choice) => JSON.stringify(choice.policy) === JSON.stringify(policy),
94
+ )?.key ?? defaultRetentionKey;
95
+ };
96
+
97
+ $effect(() => {
98
+ if (loaded || !repository) {
99
+ return;
100
+ }
101
+
102
+ loaded = true;
103
+ load();
104
+ });
105
+
106
+ const onSave = () => {
107
+ if (!integration || !repository || !schedule) {
108
+ return;
109
+ }
110
+
111
+ mutation.mutate({
112
+ name,
113
+ cron: schedule.cron,
114
+ dataFolders: integration.configuration.dataFolders,
115
+ backupConfiguration: integration.configuration.backupConfiguration,
116
+ libraries: integration.configuration.libraries,
117
+ worm,
118
+ retentionPolicy: worm
119
+ ? null
120
+ : (retentionChoices.find((choice) => choice.key === retentionKey)
121
+ ?.policy ?? null),
122
+ });
123
+ };
124
+ </script>
125
+
126
+ <OnEvents
127
+ {onIntegrationUpdate}
128
+ {onRepositoryCreate}
129
+ {onRepositoryUpdate}
130
+ {onRepositoryDelete}
131
+ {onScheduleCreate}
132
+ {onScheduleUpdate}
133
+ {onScheduleDelete}
134
+ />
135
+
136
+ <Accordion
137
+ title="Storage"
138
+ subtitle="Manage the backup name, where it is stored, and how long backups are kept."
139
+ icon={mdiDownloadBox}
140
+ >
141
+ <Stack gap={4} class="pt-2">
142
+ {#if isLoading}
143
+ <LoadingSpinner />
144
+ {:else}
145
+ <Field
146
+ label="Backup name"
147
+ description="How this backup is labelled across FUTO Backups."
148
+ required="indicator"
149
+ >
150
+ <Input bind:value={name} />
151
+ </Field>
152
+
153
+ <BackendsList {repository} />
154
+
155
+ {#if repository?.worm}
156
+ <Field
157
+ label="Write-only"
158
+ description="Once written, backups can't be removed. This can't be turned off again."
159
+ readOnly // @immich/ui forces enabled={false} when disabled={true}
160
+ >
161
+ <Switch checked class="cursor-not-allowed opacity-38" />
162
+ </Field>
163
+ {:else}
164
+ <Field
165
+ label="Write-only"
166
+ description="Once written, backups can't be removed. This can't be turned off again."
167
+ >
168
+ <Switch bind:checked={worm} />
169
+ </Field>
170
+ {/if}
171
+
172
+ <Field
173
+ label="Delete old backups"
174
+ description="Older snapshots are pruned automatically."
175
+ disabled={worm}
176
+ >
177
+ <Select
178
+ options={retentionChoices.map(({ key, label }) => ({
179
+ value: key,
180
+ label,
181
+ }))}
182
+ value={worm ? "never" : retentionKey}
183
+ onChange={(value) => (retentionKey = value)}
184
+ />
185
+ </Field>
186
+
187
+ <HStack gap={2} class="justify-end">
188
+ <Button
189
+ shape="round"
190
+ color="secondary"
191
+ disabled={mutation.isPending}
192
+ onclick={load}
193
+ >
194
+ Reset
195
+ </Button>
196
+ <Button
197
+ shape="round"
198
+ disabled={name.trim().length === 0}
199
+ loading={mutation.isPending}
200
+ onclick={onSave}
201
+ >
202
+ Save
203
+ </Button>
204
+ </HStack>
205
+ {/if}
206
+ </Stack>
207
+ </Accordion>
@@ -0,0 +1,3 @@
1
+ declare const ImmichSettingsStorage: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ImmichSettingsStorage = ReturnType<typeof ImmichSettingsStorage>;
3
+ export default ImmichSettingsStorage;
@@ -1,12 +1,11 @@
1
1
  <script lang="ts">
2
+ import { Button, Card, CardBody, HStack, Text, VStack } from "@immich/ui";
2
3
  import {
3
- Button,
4
- Card,
5
- CardBody,
6
- HStack,
7
- VStack,
8
- } from "@immich/ui";
9
- import { mdiAsterisk, mdiContentCopy, mdiFile, mdiPrinter } from "@mdi/js";
4
+ mdiAsterisk,
5
+ mdiContentCopy,
6
+ mdiDownloadOutline,
7
+ mdiPrinterOutline,
8
+ } from "@mdi/js";
10
9
 
11
10
  type Props = {
12
11
  code: string;
@@ -69,29 +68,44 @@
69
68
  };
70
69
  </script>
71
70
 
72
- <Card class="bg-primary-50 shadow-none">
71
+ <Card color="primary" class="shadow-none">
73
72
  <CardBody class="flex justify-center">
74
- <pre><code>{code}</code></pre>
73
+ <Text size="large" fontWeight="semi-bold"
74
+ ><pre><code>{code}</code></pre></Text
75
+ >
75
76
  </CardBody>
76
77
  </Card>
77
78
 
78
79
  <VStack>
79
- <HStack>
80
- <Button leadingIcon={mdiPrinter} variant="outline" onclick={print}>
81
- Print
82
- </Button>
83
- <Button leadingIcon={mdiFile} variant="outline" onclick={saveFile}>
84
- Save as file
85
- </Button>
86
- </HStack>
87
80
  <Button
88
- leadingIcon={mdiContentCopy}
81
+ shape="round"
89
82
  variant="outline"
83
+ leadingIcon={mdiContentCopy}
90
84
  onclick={copyToClipboard}
91
85
  >
92
- Copy to clipboard
86
+ Copy recovery key
93
87
  </Button>
94
- {#if PasswordCredential}
88
+
89
+ <HStack>
90
+ <Button
91
+ leadingIcon={mdiDownloadOutline}
92
+ variant="ghost"
93
+ shape="round"
94
+ onclick={saveFile}
95
+ >
96
+ Download
97
+ </Button>
98
+ <Button
99
+ leadingIcon={mdiPrinterOutline}
100
+ variant="ghost"
101
+ shape="round"
102
+ onclick={print}
103
+ >
104
+ Print
105
+ </Button>
106
+ </HStack>
107
+
108
+ {#if /* intentionally hide this to not overcrowd the modal */ PasswordCredential && false}
95
109
  <Button
96
110
  leadingIcon={mdiAsterisk}
97
111
  variant="outline"
@@ -1,20 +1,23 @@
1
1
  <script lang="ts">
2
2
  import type { OnboardingStatusResponseDto } from "../../fetch-client";
3
+ import {
4
+ handleSetupLocalStorage,
5
+ handleStartYuccaLogin,
6
+ } from "../../services/backend.service";
3
7
  import {
4
8
  handleConfirmRecoveryKey,
5
9
  handleCurrentRecoveryKey,
6
10
  handleSkipOnboardingExtraConfig,
7
11
  } from "../../services/onboarding.service";
12
+ import { Modal, ModalBody } from "@immich/ui";
8
13
  import { onMount } from "svelte";
9
14
  import CreateFirstBackup from "./stages/SampleCreateFirstBackup.svelte";
10
15
  import CreateFirstSchedule from "./stages/SampleCreateFirstSchedule.svelte";
11
- import BackupOptions from "./stages/OnboardingStageBackupServices.svelte";
12
- import ConfirmKey from "./stages/OnboardingStageKeyConfirm.svelte";
13
16
  import ImportKey from "./stages/OnboardingStageKeyImport.svelte";
14
- import KeyIntro from "./stages/OnboardingStageKeyIntro.svelte";
15
- import SaveKey from "./stages/OnboardingStageKeySave.svelte";
16
17
  import Telemetry from "./stages/OnboardingStageTelemetry.svelte";
17
- import Welcome from "./stages/OnboardingStageWelcome.svelte";
18
+ import StepFinishSetup from "./steps/OnboardingStep1FinishSetup.svelte";
19
+ import StepConnectAccount from "./steps/OnboardingStep2ConnectAccount.svelte";
20
+ import StepSaveRecoveryKey from "./steps/OnboardingStep3SaveRecoveryKey.svelte";
18
21
 
19
22
  type Props = {
20
23
  status: OnboardingStatusResponseDto;
@@ -25,21 +28,23 @@
25
28
  const { status, onFinish, onCancel }: Props = $props();
26
29
 
27
30
  let code = $state("");
31
+ let confirming = $state(false);
28
32
 
29
33
  // svelte-ignore state_referenced_locally
30
34
  let stage:
31
- | "welcome"
35
+ | "intro"
32
36
  | "telemetry"
33
- | `key-${"intro" | "save" | "confirm" | "import"}`
34
- | "backup-service"
37
+ | "key"
38
+ | "key-import"
39
+ | "connect"
35
40
  | "backup-create"
36
41
  | "schedule-create" = $state(
37
42
  !status.hasOnboardedKey
38
- ? "welcome"
43
+ ? "intro"
39
44
  : status.hasTelemetry === "none"
40
45
  ? "telemetry"
41
46
  : !status.hasBackend
42
- ? "backup-service"
47
+ ? "connect"
43
48
  : !status.hasBackup
44
49
  ? "backup-create"
45
50
  : "schedule-create",
@@ -57,56 +62,70 @@
57
62
  };
58
63
 
59
64
  const onConfirmKey = async () => {
60
- await handleConfirmRecoveryKey();
65
+ confirming = true;
66
+
67
+ try {
68
+ await handleConfirmRecoveryKey();
61
69
 
62
- if (status.hasBackend) {
63
- onFinish();
64
- } else {
65
- stage = "backup-service";
70
+ if (status.hasBackend) {
71
+ onFinish();
72
+ } else {
73
+ stage = "connect";
74
+ }
75
+ } finally {
76
+ confirming = false;
66
77
  }
67
78
  };
79
+
80
+ const onBackendReady = () => (stage = "backup-create");
68
81
  </script>
69
82
 
70
- {#if stage === "welcome"}
71
- <Welcome
72
- onNext={() => (stage = status.hasTelemetry === "none" ? "telemetry" : "key-intro")}
73
- onImportKey={() => (stage = "key-import")}
74
- {onCancel}
75
- />
83
+ {#if stage === "intro"}
84
+ <Modal size="small" title="FUTO Backups" onClose={onCancel}>
85
+ <ModalBody>
86
+ <StepFinishSetup
87
+ onContinue={() =>
88
+ (stage = status.hasTelemetry === "none" ? "telemetry" : "key")}
89
+ onImportKey={() => (stage = "key-import")}
90
+ />
91
+ </ModalBody>
92
+ </Modal>
76
93
  {:else if stage === "telemetry"}
77
94
  <Telemetry
78
95
  onContinue={() =>
79
96
  (stage = !status.hasOnboardedKey
80
- ? "key-intro"
97
+ ? "key"
81
98
  : !status.hasBackend
82
- ? "backup-service"
99
+ ? "connect"
83
100
  : !status.hasBackup
84
101
  ? "backup-create"
85
102
  : "schedule-create")}
86
103
  {onCancel}
87
104
  />
88
- {:else if stage === "key-intro"}
89
- <KeyIntro onNext={() => (stage = "key-save")} {onCancel} />
90
- {:else if stage === "key-save"}
91
- <SaveKey {code} onNext={() => (stage = "key-confirm")} {onCancel} />
92
- {:else if stage === "key-confirm"}
93
- <ConfirmKey
94
- {code}
95
- {onConfirmKey}
96
- onBack={() => (stage = "key-save")}
97
- {onCancel}
98
- />
105
+ {:else if stage === "key"}
106
+ <Modal size="small" title="FUTO Backups" onClose={onCancel}>
107
+ <ModalBody>
108
+ <StepSaveRecoveryKey {code} onContinue={onConfirmKey} loading={confirming} />
109
+ </ModalBody>
110
+ </Modal>
99
111
  {:else if stage === "key-import"}
100
112
  <ImportKey
101
- onStart={() => (stage = "welcome")}
113
+ onStart={() => (stage = "intro")}
102
114
  onImported={(key) => {
103
115
  code = key;
104
- stage = "key-save";
116
+ stage = "key";
105
117
  }}
106
118
  {onCancel}
107
119
  />
108
- {:else if stage === "backup-service"}
109
- <BackupOptions onNext={() => (stage = "backup-create")} {onCancel} />
120
+ {:else if stage === "connect"}
121
+ <Modal size="small" title="FUTO Backups" onClose={onCancel}>
122
+ <ModalBody>
123
+ <StepConnectAccount
124
+ onConnect={() => handleStartYuccaLogin(onBackendReady)}
125
+ onLocalStorage={() => handleSetupLocalStorage(onBackendReady)}
126
+ />
127
+ </ModalBody>
128
+ </Modal>
110
129
  {:else if stage === "backup-create"}
111
130
  <CreateFirstBackup onNext={() => (stage = "schedule-create")} {onSkip} />
112
131
  {:else if stage === "schedule-create"}