@futo-org/backups-orchestrator-ui 0.27.0 → 0.28.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 (156) 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 +22 -10
  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 +135 -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 +73 -0
  38. package/dist/components/integrations/immich/ImmichBackupsCard.svelte.d.ts +12 -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 +15 -3
  42. package/dist/components/integrations/immich/ImmichBackupsPage.svelte.d.ts +7 -1
  43. package/dist/components/integrations/immich/ImmichBackupsSidebarItem.svelte +66 -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 +27 -0
  73. package/dist/components/onboarding/upsell/UpsellFrequentlyAskedQuestions.svelte.d.ts +10 -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 +39 -0
  81. package/dist/components/onboarding/upsell/UpsellPage.svelte.d.ts +13 -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 +129 -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/MockImmichPhotos.svelte +36 -0
  99. package/dist/components/test/immich/MockImmichPhotos.svelte.d.ts +18 -0
  100. package/dist/components/test/immich/MockImmichPurchaseInfo.svelte +29 -0
  101. package/dist/components/test/immich/MockImmichPurchaseInfo.svelte.d.ts +18 -0
  102. package/dist/components/test/immich/MockImmichStorageSpace.svelte +37 -0
  103. package/dist/components/test/immich/MockImmichStorageSpace.svelte.d.ts +6 -0
  104. package/dist/components/ui/Accordion.svelte +67 -0
  105. package/dist/components/ui/Accordion.svelte.d.ts +11 -0
  106. package/dist/components/ui/ListToolbar.svelte +60 -0
  107. package/dist/components/ui/ListToolbar.svelte.d.ts +16 -0
  108. package/dist/components/ui/PageLayout.svelte +21 -9
  109. package/dist/components/ui/PageLayout.svelte.d.ts +1 -0
  110. package/dist/components/ui/Pagination.svelte +80 -0
  111. package/dist/components/ui/Pagination.svelte.d.ts +8 -0
  112. package/dist/components/ui/PathListField.svelte +17 -14
  113. package/dist/components/ui/StackList.svelte +18 -18
  114. package/dist/components/ui/StackList.svelte.d.ts +6 -27
  115. package/dist/components/ui/StackListItem.svelte +47 -25
  116. package/dist/components/ui/StackListItem.svelte.d.ts +5 -2
  117. package/dist/components/ui/StackListOption.svelte +33 -0
  118. package/dist/components/ui/StackListOption.svelte.d.ts +11 -0
  119. package/dist/components/ui/StackListPlaceholder.svelte +12 -0
  120. package/dist/components/ui/StackListPlaceholder.svelte.d.ts +7 -0
  121. package/dist/components/ui/StepNumber.svelte +20 -0
  122. package/dist/components/ui/StepNumber.svelte.d.ts +7 -0
  123. package/dist/components/util/Suspense.svelte +7 -3
  124. package/dist/components/util/YuccaContext.svelte +1 -1
  125. package/dist/events.js +1 -1
  126. package/dist/fetch-client.d.ts +2 -0
  127. package/dist/index.d.ts +6 -1
  128. package/dist/index.js +6 -1
  129. package/dist/services/backend.service.js +3 -0
  130. package/dist/services/immich.integration.service.d.ts +1 -1
  131. package/dist/services/immich.integration.service.js +2 -3
  132. package/dist/services/integrations.service.d.ts +1 -0
  133. package/dist/services/integrations.service.js +2 -1
  134. package/dist/services/log.service.svelte.d.ts +12 -0
  135. package/dist/services/log.service.svelte.js +15 -1
  136. package/dist/services/repository.service.js +9 -3
  137. package/dist/services/runHistory.service.js +2 -2
  138. package/dist/utils/format.d.ts +1 -1
  139. package/dist/utils/format.js +2 -2
  140. package/package.json +3 -3
  141. package/dist/components/backups/dialogs/ViewLogModal.svelte +0 -208
  142. package/dist/components/backups/dialogs/ViewLogModal.svelte.d.ts +0 -7
  143. package/dist/components/integrations/immich/ImmichConfigureBackup.svelte +0 -403
  144. package/dist/components/integrations/immich/ImmichConfigureBackup.svelte.d.ts +0 -9
  145. package/dist/components/integrations/immich/ImmichConfirmDefaultBackup.svelte +0 -80
  146. package/dist/components/integrations/immich/ImmichConfirmDefaultBackup.svelte.d.ts +0 -8
  147. package/dist/components/onboarding/stages/OnboardingStageKeyConfirm.svelte +0 -56
  148. package/dist/components/onboarding/stages/OnboardingStageKeyConfirm.svelte.d.ts +0 -9
  149. package/dist/components/onboarding/stages/OnboardingStageKeyIntro.svelte +0 -50
  150. package/dist/components/onboarding/stages/OnboardingStageKeyIntro.svelte.d.ts +0 -7
  151. package/dist/components/onboarding/stages/OnboardingStageKeySave.svelte +0 -44
  152. package/dist/components/onboarding/stages/OnboardingStageKeySave.svelte.d.ts +0 -8
  153. package/dist/components/onboarding/stages/OnboardingStageWelcome.svelte +0 -56
  154. package/dist/components/onboarding/stages/OnboardingStageWelcome.svelte.d.ts +0 -9
  155. package/dist/components/util/TimedButton.svelte +0 -37
  156. package/dist/components/util/TimedButton.svelte.d.ts +0 -7
@@ -1,6 +1,6 @@
1
1
  import { type Snippet } from "svelte";
2
2
  type Props = {
3
- onExit: () => void;
3
+ fallback: Snippet<[() => void]>;
4
4
  children: Snippet;
5
5
  };
6
6
  declare const ImmichOnboardingSetupFlow: import("svelte").Component<Props, {}, "">;
@@ -0,0 +1,313 @@
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
+ Checkbox,
20
+ Field,
21
+ HStack,
22
+ LoadingSpinner,
23
+ Select,
24
+ Stack,
25
+ Switch,
26
+ Text,
27
+ } from "@immich/ui";
28
+ import { mdiImageMultiple } from "@mdi/js";
29
+ import { SvelteSet } from "svelte/reactivity";
30
+
31
+ type Group = {
32
+ key: string;
33
+ label: string;
34
+ description: string;
35
+ folders: string[];
36
+ required?: boolean;
37
+ warning?: string;
38
+ warningColor?: "danger" | "warning";
39
+ };
40
+
41
+ const groups: Group[] = [
42
+ {
43
+ key: "media",
44
+ label: "Photos and videos",
45
+ description: "Your media uploaded directly to Immich.",
46
+ folders: ["upload", "profile", "library"],
47
+ warning: "It is highly recommended you back this up!",
48
+ warningColor: "danger",
49
+ },
50
+ {
51
+ key: "database",
52
+ label: "Database and metadata",
53
+ description:
54
+ "Albums, people, tags, favorites and other library details. Required to restore.",
55
+ folders: ["backups"],
56
+ required: true,
57
+ },
58
+ {
59
+ key: "thumbs",
60
+ label: "Thumbnails and previews",
61
+ description: "Generated photo previews, can be recreated later.",
62
+ folders: ["thumbs"],
63
+ warning:
64
+ "When restoring, you will see broken previews. Use the admin panel to regenerate them.",
65
+ warningColor: "warning",
66
+ },
67
+ {
68
+ key: "encoded",
69
+ label: "Encoded videos",
70
+ description: "Generated video previews, can be recreated later.",
71
+ folders: ["encoded-video"],
72
+ warning:
73
+ "When restoring, you will see broken previews. Use the admin panel to regenerate them.",
74
+ warningColor: "warning",
75
+ },
76
+ ];
77
+
78
+ const integrations = useIntegrations();
79
+ const repositories = useRepositories();
80
+ const schedules = useSchedules();
81
+ const mutation = useConfigureImmichIntegration();
82
+
83
+ const { onIntegrationUpdate } = useIntegrationEventHandler();
84
+ const { onRepositoryCreate, onRepositoryUpdate, onRepositoryDelete } =
85
+ useRepositoryEventHandler();
86
+ const { onScheduleCreate, onScheduleUpdate, onScheduleDelete } =
87
+ useScheduleEventHandler();
88
+
89
+ const integration = $derived(integrations.data?.immichIntegration);
90
+
91
+ const repository = $derived(
92
+ integration
93
+ ? repositories.data?.find((entry) => entry.id === integration.id)
94
+ : undefined,
95
+ );
96
+
97
+ const schedule = $derived(
98
+ integration
99
+ ? schedules.data?.find((entry) => entry.id === integration.scheduleId)
100
+ : undefined,
101
+ );
102
+
103
+ const isLoading = $derived(
104
+ integrations.isLoading || repositories.isLoading || schedules.isLoading,
105
+ );
106
+
107
+ type LibrariesMode = "all" | "none" | "some";
108
+
109
+ const libraryModes = [
110
+ { value: "all", label: "All libraries" },
111
+ { value: "none", label: "No libraries" },
112
+ { value: "some", label: "Choose libraries" },
113
+ ];
114
+
115
+ const folders = new SvelteSet<string>();
116
+ const selectedLibraries = new SvelteSet<string>();
117
+ let backupConfiguration = $state(true);
118
+ let librariesMode = $state<LibrariesMode>("all");
119
+ let loaded = false;
120
+
121
+ $effect(() => {
122
+ if (loaded || !integration) {
123
+ return;
124
+ }
125
+
126
+ loaded = true;
127
+ backupConfiguration = integration.configuration.backupConfiguration;
128
+
129
+ const libraries = integration.configuration.libraries;
130
+
131
+ if (libraries === "all") {
132
+ librariesMode = "all";
133
+ } else if (libraries.length === 0) {
134
+ librariesMode = "none";
135
+ } else {
136
+ librariesMode = "some";
137
+
138
+ for (const id of libraries) {
139
+ selectedLibraries.add(id);
140
+ }
141
+ }
142
+
143
+ for (const folder of integration.configuration.dataFolders) {
144
+ folders.add(folder);
145
+ }
146
+ });
147
+
148
+ const toggleLibrary = (id: string) => {
149
+ if (selectedLibraries.has(id)) {
150
+ selectedLibraries.delete(id);
151
+ } else {
152
+ selectedLibraries.add(id);
153
+ }
154
+ };
155
+
156
+ const available = $derived(
157
+ integrations.data?.immichState?.dataFolders ?? [],
158
+ );
159
+
160
+ const libraries = $derived(integrations.data?.immichState?.libraries ?? []);
161
+
162
+ const applicable = (group: Group) =>
163
+ group.folders.filter((folder) => available.includes(folder));
164
+
165
+ const isChecked = (group: Group) => {
166
+ const own = applicable(group);
167
+
168
+ return own.length > 0 && own.every((folder) => folders.has(folder));
169
+ };
170
+
171
+ const toggle = (group: Group) => {
172
+ const own = applicable(group);
173
+ const enabled = own.every((folder) => folders.has(folder));
174
+
175
+ for (const folder of own) {
176
+ if (enabled) {
177
+ folders.delete(folder);
178
+ } else {
179
+ folders.add(folder);
180
+ }
181
+ }
182
+ };
183
+
184
+ const onSave = () => {
185
+ folders.add("backups");
186
+
187
+ if (!integration || !repository || !schedule) {
188
+ return;
189
+ }
190
+
191
+ mutation.mutate({
192
+ name: repository.name,
193
+ worm: repository.worm,
194
+ cron: schedule.cron,
195
+ retentionPolicy: repository.configuration?.retentionPolicy ?? null,
196
+ dataFolders: [...folders],
197
+ backupConfiguration,
198
+ libraries:
199
+ librariesMode === "all"
200
+ ? "all"
201
+ : librariesMode === "none"
202
+ ? []
203
+ : [...selectedLibraries],
204
+ });
205
+ };
206
+ </script>
207
+
208
+ <OnEvents
209
+ {onIntegrationUpdate}
210
+ {onRepositoryCreate}
211
+ {onRepositoryUpdate}
212
+ {onRepositoryDelete}
213
+ {onScheduleCreate}
214
+ {onScheduleUpdate}
215
+ {onScheduleDelete}
216
+ />
217
+
218
+ <Accordion
219
+ title="Backup Contents"
220
+ subtitle="Choose what Immich includes in each backup."
221
+ icon={mdiImageMultiple}
222
+ >
223
+ <Stack gap={4} class="pt-2">
224
+ {#if isLoading}
225
+ <LoadingSpinner />
226
+ {:else}
227
+ {#each groups as group (group.key)}
228
+ {#if applicable(group).length > 0}
229
+ <Stack gap={1}>
230
+ <Field
231
+ label={group.label}
232
+ description={group.description}
233
+ readOnly={group.required}
234
+ required={group.required ? "indicator" : undefined}
235
+ >
236
+ <Switch
237
+ checked={group.required || isChecked(group)}
238
+ class={group.required
239
+ ? "cursor-not-allowed opacity-38"
240
+ : undefined}
241
+ onCheckedChange={() => toggle(group)}
242
+ />
243
+ </Field>
244
+
245
+ {#if group.warning && !group.required && !isChecked(group)}
246
+ <Text size="small" color={group.warningColor}>
247
+ {group.warning}
248
+ </Text>
249
+ {/if}
250
+ </Stack>
251
+ {/if}
252
+ {/each}
253
+
254
+ <Stack gap={1}>
255
+ <Field
256
+ label="Backup configuration"
257
+ description="Include these backup settings, so they survive a restore."
258
+ >
259
+ <Switch bind:checked={backupConfiguration} />
260
+ </Field>
261
+
262
+ {#if !backupConfiguration}
263
+ <Text size="small" color="warning">
264
+ You will need to reconfigure backups from scratch after restoring.
265
+ </Text>
266
+ {/if}
267
+ </Stack>
268
+
269
+ <Field
270
+ label="External libraries"
271
+ description="Libraries stored outside of Immich. New libraries are included automatically unless you choose specific ones."
272
+ >
273
+ <Select
274
+ options={libraryModes}
275
+ value={librariesMode}
276
+ onChange={(value) => (librariesMode = value as LibrariesMode)}
277
+ />
278
+ </Field>
279
+
280
+ {#if librariesMode === "some"}
281
+ {#if libraries.length === 0}
282
+ <Text size="small" color="muted">No external libraries found.</Text>
283
+ {:else}
284
+ <Stack gap={2} class="ps-1">
285
+ {#each libraries as library (library.id)}
286
+ <label class="flex select-none items-center gap-3">
287
+ <Checkbox
288
+ checked={selectedLibraries.has(library.id)}
289
+ onCheckedChange={() => toggleLibrary(library.id)}
290
+ />
291
+ <Text size="small">{library.name}</Text>
292
+ </label>
293
+ {/each}
294
+ </Stack>
295
+ {/if}
296
+ {:else if librariesMode === "all" && libraries.length > 0}
297
+ <Text size="small" color="muted">
298
+ Included: {libraries.map((library) => library.name).join(", ")}
299
+ </Text>
300
+ {/if}
301
+
302
+ <HStack class="justify-end">
303
+ <Button
304
+ shape="round"
305
+ loading={mutation.isPending}
306
+ onclick={onSave}
307
+ >
308
+ Save
309
+ </Button>
310
+ </HStack>
311
+ {/if}
312
+ </Stack>
313
+ </Accordion>
@@ -0,0 +1,3 @@
1
+ declare const ImmichSettingsBackupContents: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ImmichSettingsBackupContents = ReturnType<typeof ImmichSettingsBackupContents>;
3
+ export default ImmichSettingsBackupContents;
@@ -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;