@futo-org/backups-orchestrator-ui 0.26.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
@@ -9,7 +9,7 @@
9
9
  getBackendActions,
10
10
  handleStartYuccaLogin,
11
11
  } from "../../services/backend.service";
12
- import { Badge, Icon, Text } from "@immich/ui";
12
+ import { Badge, Icon } from "@immich/ui";
13
13
  import { mdiCloudOutline, mdiHarddisk, mdiShieldCheckOutline } from "@mdi/js";
14
14
  import StackListItem from "../ui/StackListItem.svelte";
15
15
 
@@ -33,6 +33,22 @@
33
33
  s3: "S3 Server",
34
34
  };
35
35
 
36
+ const BackendDescriptions: Record<BackendType, string> = {
37
+ yucca: "Hosted cloud backup storage",
38
+ local: "A folder on this computer",
39
+ s3: "An S3-compatible server",
40
+ };
41
+
42
+ const title = $derived(
43
+ repositoryBackend?.primary
44
+ ? `${BackendNames[backend.type]} (primary)`
45
+ : BackendNames[backend.type],
46
+ );
47
+
48
+ const online = $derived(
49
+ backend.isOnline && (!repositoryBackend || repositoryBackend.online),
50
+ );
51
+
36
52
  const { LoginAgain, Reconfigure } = $derived(
37
53
  getBackendActions(repository, backend, repositoryBackend, () =>
38
54
  handleStartYuccaLogin(),
@@ -40,34 +56,22 @@
40
56
  );
41
57
  </script>
42
58
 
43
- <StackListItem actions={[LoginAgain, Reconfigure]}>
59
+ <StackListItem {title} actions={[LoginAgain, Reconfigure]}>
44
60
  {#snippet icon()}
45
61
  <Icon icon={BackendIcons[backend.type]} />
46
62
  {/snippet}
47
63
 
48
- <Text>{BackendNames[backend.type]}</Text>
49
-
50
- {#if repositoryBackend?.primary}
51
- <Text size="small">(primary)</Text>
52
- {/if}
64
+ {backend.description ?? BackendDescriptions[backend.type]}
53
65
 
54
66
  {#snippet trailing()}
55
- {#if repositoryBackend}
56
- <Badge
57
- color={backend.isOnline && repositoryBackend.online
58
- ? "success"
59
- : "danger"}
60
- size="small"
61
- >{backend.isOnline
62
- ? repositoryBackend.online
63
- ? "Active"
64
- : "Missing on service"
65
- : "Offline"}</Badge
66
- >
67
- {:else}
68
- <Badge color={backend.isOnline ? "success" : "danger"} size="small"
69
- >{backend.isOnline ? "Online" : "Offline"}</Badge
70
- >
71
- {/if}
67
+ <Badge color={online ? "primary" : "danger"} size="small">
68
+ {#if !backend.isOnline}
69
+ Offline
70
+ {:else if repositoryBackend}
71
+ {repositoryBackend.online ? "Active" : "Missing on service"}
72
+ {:else}
73
+ Online
74
+ {/if}
75
+ </Badge>
72
76
  {/snippet}
73
77
  </StackListItem>
@@ -15,9 +15,10 @@
15
15
 
16
16
  type Props = {
17
17
  repository?: LocalRepositoryDto;
18
+ actions?: boolean;
18
19
  };
19
20
 
20
- const { repository }: Props = $props();
21
+ const { repository, actions = false }: Props = $props();
21
22
 
22
23
  const { advanced } = options;
23
24
  const query = useBackends();
@@ -38,39 +39,37 @@
38
39
 
39
40
  <OnEvents {onBackendCreate} />
40
41
 
41
- <Suspense {query}>
42
- <StackList>
43
- {#snippet title()}
44
- {#if repository}
45
- Where your backup is stored
46
- {:else}
47
- Configured backends
48
- {/if}
49
- {/snippet}
42
+ {#snippet heading()}
43
+ Where your backup is stored
44
+ {/snippet}
50
45
 
51
- {#each query.data as backend (backend.id)}
52
- {@const repositoryBackend = repositoryBackends.find(
53
- ({ id }) => backend.id === id,
54
- )}
46
+ <StackList title={repository ? heading : undefined}>
47
+ <Suspense {query}>
48
+ {#snippet children(backends)}
49
+ {#each backends as backend (backend.id)}
50
+ {@const repositoryBackend = repositoryBackends.find(
51
+ ({ id }) => backend.id === id,
52
+ )}
55
53
 
56
- {#if typeof repository === typeof repositoryBackend}
57
- <BackendItem {repository} {backend} {repositoryBackend} />
58
- {/if}
59
- {/each}
60
- </StackList>
54
+ {#if typeof repository === typeof repositoryBackend}
55
+ <BackendItem {repository} {backend} {repositoryBackend} />
56
+ {/if}
57
+ {/each}
58
+ {/snippet}
59
+ </Suspense>
60
+ </StackList>
61
61
 
62
- {#if $advanced}
63
- <HStack>
64
- <Button
65
- size="small"
66
- variant="outline"
67
- onclick={() => handleStartYuccaLogin()}>Login with FUTO Backups</Button
68
- >
69
- <Button
70
- size="small"
71
- variant="outline"
72
- onclick={() => handleSetupLocalStorage()}>New local storage</Button
73
- >
74
- </HStack>
75
- {/if}
76
- </Suspense>
62
+ {#if actions || $advanced}
63
+ <HStack>
64
+ <Button
65
+ size="small"
66
+ variant="outline"
67
+ onclick={() => handleStartYuccaLogin()}>Login with FUTO Backups</Button
68
+ >
69
+ <Button
70
+ size="small"
71
+ variant="outline"
72
+ onclick={() => handleSetupLocalStorage()}>New local storage</Button
73
+ >
74
+ </HStack>
75
+ {/if}
@@ -1,6 +1,7 @@
1
1
  import type { LocalRepositoryDto } from "../../fetch-client";
2
2
  type Props = {
3
3
  repository?: LocalRepositoryDto;
4
+ actions?: boolean;
4
5
  };
5
6
  declare const BackendsList: import("svelte").Component<Props, {}, "">;
6
7
  type BackendsList = ReturnType<typeof BackendsList>;
@@ -1,6 +1,6 @@
1
1
  <script lang="ts">
2
2
  import StackList from "../../ui/StackList.svelte";
3
- import StackListItem from "../../ui/StackListItem.svelte";
3
+ import StackListOption from "../../ui/StackListOption.svelte";
4
4
  import Suspense from "../../util/Suspense.svelte";
5
5
  import {
6
6
  handleSetupLocalStorage,
@@ -15,9 +15,8 @@
15
15
  ModalBody,
16
16
  ModalFooter,
17
17
  Stack,
18
- Text,
19
18
  } from "@immich/ui";
20
- import { mdiChevronRight, mdiHarddisk, mdiShieldCheck } from "@mdi/js";
19
+ import { mdiHarddisk, mdiShieldCheck } from "@mdi/js";
21
20
  import type { Snippet } from "svelte";
22
21
 
23
22
  type Props = {
@@ -61,62 +60,52 @@
61
60
  <Stack>
62
61
  {@render leadingContent?.()}
63
62
 
64
- <Suspense query={backends}>
65
- <StackList>
66
- <StackListItem {disabled} onclick={onFutoBackups}>
67
- {#snippet icon()}
68
- <Icon icon={mdiShieldCheck} size="36px" />
69
- {/snippet}
63
+ <StackList>
64
+ <StackListOption
65
+ {disabled}
66
+ title="FUTO Backups"
67
+ onclick={onFutoBackups}
68
+ >
69
+ {#snippet icon()}
70
+ <Icon icon={mdiShieldCheck} />
71
+ {/snippet}
70
72
 
71
- <Stack gap={0}>
72
- <Text class="font-bold">FUTO Backups</Text>
73
- <Text>Simple, hosted backups.</Text>
74
- </Stack>
73
+ Simple, hosted backups.
74
+ </StackListOption>
75
+ <StackListOption
76
+ {disabled}
77
+ title="Local Storage"
78
+ onclick={onLocalBackups}
79
+ >
80
+ {#snippet icon()}
81
+ <Icon icon={mdiHarddisk} />
82
+ {/snippet}
75
83
 
76
- {#snippet trailing()}
77
- <Icon icon={mdiChevronRight} />
78
- {/snippet}
79
- </StackListItem>
80
- <StackListItem {disabled} onclick={onLocalBackups}>
81
- {#snippet icon()}
82
- <Icon icon={mdiHarddisk} size="36px" />
83
- {/snippet}
84
+ A folder on this computer.
85
+ </StackListOption>
84
86
 
85
- <Stack gap={0}>
86
- <Text class="font-bold">Local Storage</Text>
87
- <Text>A folder on this computer.</Text>
88
- </Stack>
87
+ <Suspense query={backends}>
88
+ {#snippet children(available)}
89
+ {#each available as backend (backend.id)}
90
+ {#if backend.type === "local"}
91
+ <StackListOption
92
+ {disabled}
93
+ title="Existing Local Storage"
94
+ onclick={() => {
95
+ onSelect(backend.id);
96
+ }}
97
+ >
98
+ {#snippet icon()}
99
+ <Icon icon={mdiHarddisk} />
100
+ {/snippet}
89
101
 
90
- {#snippet trailing()}
91
- <Icon icon={mdiChevronRight} />
92
- {/snippet}
93
- </StackListItem>
94
-
95
- {#each backends.data as backend}
96
- {#if backend.type === "local"}
97
- <StackListItem
98
- {disabled}
99
- onclick={() => {
100
- onSelect(backend.id);
101
- }}
102
- >
103
- {#snippet icon()}
104
- <Icon icon={mdiHarddisk} size="36px" />
105
- {/snippet}
106
-
107
- <Stack gap={0}>
108
- <Text class="font-bold">Existing Local Storage</Text>
109
- <Text>{backend.description}</Text>
110
- </Stack>
111
-
112
- {#snippet trailing()}
113
- <Icon icon={mdiChevronRight} />
114
- {/snippet}
115
- </StackListItem>
116
- {/if}
117
- {/each}
118
- </StackList>
119
- </Suspense>
102
+ {backend.description}
103
+ </StackListOption>
104
+ {/if}
105
+ {/each}
106
+ {/snippet}
107
+ </Suspense>
108
+ </StackList>
120
109
  </Stack>
121
110
  </ModalBody>
122
111
  <ModalFooter>
@@ -1,7 +1,8 @@
1
1
  <script lang="ts">
2
2
  import type { LocalRepositoryDto } from "../../fetch-client";
3
3
  import { getRepositoryActions } from "../../services/repository.service";
4
- import { Badge, FormatBytes, Text } from "@immich/ui";
4
+ import { Badge, FormatBytes, Icon } from "@immich/ui";
5
+ import { mdiArchiveOutline } from "@mdi/js";
5
6
  import StackListItem from "../ui/StackListItem.svelte";
6
7
  import RelativeTime from "../util/RelativeTime.svelte";
7
8
 
@@ -11,48 +12,54 @@
11
12
 
12
13
  const { repository }: Props = $props();
13
14
 
15
+ const BackendNames = {
16
+ yucca: "FUTO Backups",
17
+ local: "Local Storage",
18
+ s3: "S3 Server",
19
+ };
20
+
21
+ const failed = $derived(
22
+ repository.metrics.lastBackup &&
23
+ (!repository.metrics.lastSuccessfulBackup ||
24
+ +new Date(repository.metrics.lastBackup) >
25
+ +new Date(repository.metrics.lastSuccessfulBackup)),
26
+ );
27
+
14
28
  const { BackupNow, Snapshots, History, Configure, Import, MetricsHistory } =
15
29
  $derived(getRepositoryActions(repository));
16
30
  </script>
17
31
 
18
32
  <StackListItem
33
+ title={repository.name}
34
+ color={failed ? "danger" : "primary"}
19
35
  actions={[BackupNow, Snapshots, History, Configure, Import, MetricsHistory]}
20
36
  >
21
- <Text>{repository.name}</Text>
37
+ {#snippet icon()}
38
+ <Icon icon={mdiArchiveOutline} />
39
+ {/snippet}
22
40
 
23
41
  {#if repository.backends}
24
- <Badge size="tiny" color="info">
25
- {#if repository.backends.primary.type === "yucca"}
26
- FUTO Backups
27
- {:else if repository.backends.primary.type === "local"}
28
- Local Storage
29
- {:else}
30
- S3 Server
31
- {/if}
32
- </Badge>
33
- {#if !repository.backends.primary.online}
34
- <Badge size="tiny" color="danger">Offline</Badge>
35
- {/if}
36
- {/if}
37
-
38
- {#if repository.worm}
39
- <Badge size="tiny" color="info">WORM</Badge>
42
+ {BackendNames[repository.backends.primary.type]} &middot;
40
43
  {/if}
41
44
 
42
45
  {#if repository.meter}
43
- <Badge size="tiny" color="secondary">
44
- <FormatBytes bytes={repository.meter?.sizeBytes} />
45
- </Badge>
46
+ <FormatBytes bytes={repository.meter.sizeBytes} />
46
47
  {:else}
47
- <Badge size="tiny" color="secondary">
48
- Estimated <FormatBytes bytes={repository.metrics.sizeBytes} />
49
- </Badge>
48
+ Estimated <FormatBytes bytes={repository.metrics.sizeBytes} />
49
+ {/if}
50
+
51
+ {#if repository.worm}
52
+ &middot; write-only
50
53
  {/if}
51
54
 
52
55
  {#snippet trailing()}
53
- {#if repository.metrics.lastBackup && (!repository.metrics.lastSuccessfulBackup || +new Date(repository.metrics.lastBackup) > +new Date(repository.metrics.lastSuccessfulBackup))}
56
+ {#if repository.backends && !repository.backends.primary.online}
57
+ <Badge size="tiny" color="danger">Offline</Badge>
58
+ {/if}
59
+
60
+ {#if failed}
54
61
  <Badge size="tiny" color="danger">
55
- Failed <RelativeTime time={repository.metrics.lastBackup} />
62
+ Failed <RelativeTime time={repository.metrics.lastBackup!} />
56
63
  </Badge>
57
64
  {:else if repository.metrics.lastSuccessfulBackup}
58
65
  <Badge size="tiny" color="success">
@@ -0,0 +1,200 @@
1
+ <script module lang="ts">
2
+ export type BackupStatusType = "backup" | "restore" | "forget";
3
+
4
+ export type BackupStatusState =
5
+ | "connecting"
6
+ | "running"
7
+ | "complete"
8
+ | "failed";
9
+ </script>
10
+
11
+ <script lang="ts">
12
+ import RelativeTime from "../util/RelativeTime.svelte";
13
+ import {
14
+ Alert,
15
+ Button,
16
+ Heading,
17
+ HStack,
18
+ ProgressBar,
19
+ Scrollable,
20
+ Stack,
21
+ Text,
22
+ } from "@immich/ui";
23
+ import type { Snippet } from "svelte";
24
+
25
+ type Props = {
26
+ title: string;
27
+ type: BackupStatusType;
28
+ state: BackupStatusState;
29
+ progress?: number;
30
+ start?: string;
31
+ duration?: string;
32
+ errors?: string[];
33
+ details?: Snippet;
34
+ currentFiles?: string[];
35
+ advanced?: Snippet;
36
+ onRetry?: () => void;
37
+ };
38
+
39
+ const {
40
+ title,
41
+ type,
42
+ state: backupState,
43
+ progress = 0,
44
+ start,
45
+ duration,
46
+ errors = [],
47
+ details,
48
+ currentFiles = [],
49
+ advanced,
50
+ onRetry,
51
+ }: Props = $props();
52
+
53
+ let showErrors = $state(false);
54
+
55
+ const phases: Record<BackupStatusType, [string, string, string]> = {
56
+ backup: ["Preparing backup", "Backing up", "Finalizing backup"],
57
+ restore: ["Preparing restore", "Restoring", "Finalizing restore"],
58
+ forget: ["Preparing prune", "Pruning", "Finalizing prune"],
59
+ };
60
+
61
+ const succeeded: Record<BackupStatusType, string> = {
62
+ backup: "Your library was backed up successfully",
63
+ restore: "Your library was restored successfully",
64
+ forget: "Old backups were pruned successfully",
65
+ };
66
+
67
+ const failed: Record<BackupStatusType, string> = {
68
+ backup: "Your library could not be backed up",
69
+ restore: "Your library could not be restored",
70
+ forget: "Old backups could not be pruned",
71
+ };
72
+
73
+ const reassurance: Record<BackupStatusType, string> = {
74
+ backup: "No changes were made to your existing backups.",
75
+ restore: "Your backups are untouched — nothing was lost.",
76
+ forget: "No backups were removed.",
77
+ };
78
+
79
+ const running: Record<BackupStatusType, string> = {
80
+ backup:
81
+ "You can close this window and the backup will continue in the background.",
82
+ restore:
83
+ "You can close this window and the restore will continue in the background.",
84
+ forget:
85
+ "You can close this window and the prune will continue in the background.",
86
+ };
87
+
88
+ const phase = $derived(
89
+ progress <= 0
90
+ ? phases[type][0]
91
+ : progress >= 0.95
92
+ ? phases[type][2]
93
+ : phases[type][1],
94
+ );
95
+
96
+ const headline = $derived.by(() => {
97
+ switch (backupState) {
98
+ case "connecting": {
99
+ return "Connecting…";
100
+ }
101
+ case "failed": {
102
+ return failed[type];
103
+ }
104
+ case "complete": {
105
+ return succeeded[type];
106
+ }
107
+ default: {
108
+ return `${phase} · ${Math.round(progress * 100)}%`;
109
+ }
110
+ }
111
+ });
112
+
113
+ const titleColor = $derived(
114
+ backupState === "complete" ? "success" : backupState === "failed" ? "danger" : "primary",
115
+ );
116
+
117
+ const terminal = $derived(backupState === "complete" || backupState === "failed");
118
+ </script>
119
+
120
+ <Stack gap={4}>
121
+ <Stack gap={2}>
122
+ <Heading size="medium" color={titleColor} fontWeight="bold">{title}</Heading>
123
+
124
+ <Stack gap={1}>
125
+ <Heading size="small">{headline}</Heading>
126
+
127
+ {#if backupState === "running" && start}
128
+ <Text color="muted">Started <RelativeTime time={start} /></Text>
129
+ {:else if backupState === "failed"}
130
+ <Text color="muted">{reassurance[type]}</Text>
131
+ {:else if duration}
132
+ <Text color="muted">Completed in {duration}</Text>
133
+ {/if}
134
+
135
+ {#if backupState === "complete" && errors.length > 0}
136
+ <Text color="warning">
137
+ Completed with {errors.length}
138
+ {errors.length === 1 ? "warning" : "warnings"}.
139
+ </Text>
140
+ {/if}
141
+ </Stack>
142
+ </Stack>
143
+
144
+ {#if !terminal}
145
+ <ProgressBar
146
+ {progress}
147
+ shape="round"
148
+ size="tiny"
149
+ class="bg-primary-100 border-none"
150
+ />
151
+ {/if}
152
+
153
+ {#if terminal && (errors.length > 0 || (backupState === "failed" && onRetry))}
154
+ <HStack gap={4} class="items-center">
155
+ {#if backupState === "failed" && onRetry}
156
+ <Button shape="round" onclick={onRetry}>Try again</Button>
157
+ {/if}
158
+
159
+ {#if errors.length > 0}
160
+ <Button
161
+ variant="ghost"
162
+ shape="round"
163
+ onclick={() => (showErrors = !showErrors)}
164
+ >
165
+ {showErrors ? "Hide details" : "View details"}
166
+ </Button>
167
+ {/if}
168
+ </HStack>
169
+
170
+ {#if showErrors}
171
+ <Scrollable class="max-h-32">
172
+ <Stack gap={1}>
173
+ {#each errors as error, index (index)}
174
+ <Alert color={backupState === "failed" ? "danger" : "warning"}>
175
+ {error}
176
+ </Alert>
177
+ {/each}
178
+ </Stack>
179
+ </Scrollable>
180
+ {/if}
181
+ {/if}
182
+
183
+ {#if backupState === "running"}
184
+ <Text color="muted">{running[type]}</Text>
185
+ {:else if backupState === "complete" && details}
186
+ <Text color="muted">{@render details()}</Text>
187
+ {/if}
188
+
189
+ {#if backupState === "running" && currentFiles.length > 0}
190
+ <Stack gap={1}>
191
+ {#each currentFiles.slice(0, 3) as file, index (index)}
192
+ <Text size="small" color="muted" class="truncate" title={file}>
193
+ {file}
194
+ </Text>
195
+ {/each}
196
+ </Stack>
197
+ {/if}
198
+
199
+ {@render advanced?.()}
200
+ </Stack>
@@ -0,0 +1,19 @@
1
+ export type BackupStatusType = "backup" | "restore" | "forget";
2
+ export type BackupStatusState = "connecting" | "running" | "complete" | "failed";
3
+ import type { Snippet } from "svelte";
4
+ type Props = {
5
+ title: string;
6
+ type: BackupStatusType;
7
+ state: BackupStatusState;
8
+ progress?: number;
9
+ start?: string;
10
+ duration?: string;
11
+ errors?: string[];
12
+ details?: Snippet;
13
+ currentFiles?: string[];
14
+ advanced?: Snippet;
15
+ onRetry?: () => void;
16
+ };
17
+ declare const BackupStatus: import("svelte").Component<Props, {}, "">;
18
+ type BackupStatus = ReturnType<typeof BackupStatus>;
19
+ export default BackupStatus;
@@ -4,9 +4,11 @@
4
4
  useRepositories,
5
5
  useRepositoryEventHandler,
6
6
  } from "../../services/repository.service";
7
- import { Button, HStack, modalManager, Stack, Text } from "@immich/ui";
7
+ import { Button, HStack, modalManager, Stack } from "@immich/ui";
8
8
  import StackList from "../ui/StackList.svelte";
9
+ import StackListPlaceholder from "../ui/StackListPlaceholder.svelte";
9
10
  import OnEvents from "../util/OnEvents.svelte";
11
+ import Suspense from "../util/Suspense.svelte";
10
12
  import BackupItem from "./BackupItem.svelte";
11
13
  import CreateRepositoryModal from "./dialogs/CreateRepositoryModal.svelte";
12
14
 
@@ -39,18 +41,20 @@
39
41
  <Stack gap={6}>
40
42
  {#if local}
41
43
  <Stack gap={2}>
42
- <StackList {query}>
44
+ <StackList>
43
45
  {#snippet title()}Backups on this machine{/snippet}
44
- {#snippet children()}
46
+
47
+ <Suspense {query}>
45
48
  {#each localRepositories as repository (repository.id)}
46
49
  <BackupItem {repository} />
47
50
  {/each}
51
+
48
52
  {#if localRepositories.length === 0}
49
- <Text class="text-center py-6" color="muted">
53
+ <StackListPlaceholder>
50
54
  No backups on this machine yet.
51
- </Text>
55
+ </StackListPlaceholder>
52
56
  {/if}
53
- {/snippet}
57
+ </Suspense>
54
58
  </StackList>
55
59
 
56
60
  <HStack>
@@ -64,19 +68,21 @@
64
68
  </Stack>
65
69
  {/if}
66
70
 
67
- <StackList {query}>
71
+ <StackList>
68
72
  {#snippet title()}
69
73
  {local ? "Backups found elsewhere" : "Your Backups"}
70
74
  {/snippet}
71
- {#snippet children()}
75
+
76
+ <Suspense {query}>
72
77
  {#each remoteRepositories as repository (repository.id)}
73
78
  <BackupItem {repository} />
74
79
  {/each}
80
+
75
81
  {#if remoteRepositories.length === 0}
76
- <Text class="text-center py-6" color="muted">
82
+ <StackListPlaceholder>
77
83
  {local ? "No other backups found." : "No backups yet."}
78
- </Text>
84
+ </StackListPlaceholder>
79
85
  {/if}
80
- {/snippet}
86
+ </Suspense>
81
87
  </StackList>
82
88
  </Stack>