@futo-org/backups-orchestrator-ui 0.1.71

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 (192) hide show
  1. package/LICENSE +41 -0
  2. package/dist/components/backends/BackendItem.svelte +64 -0
  3. package/dist/components/backends/BackendItem.svelte.d.ts +10 -0
  4. package/dist/components/backends/BackendsList.svelte +73 -0
  5. package/dist/components/backends/BackendsList.svelte.d.ts +7 -0
  6. package/dist/components/backends/CreateLocalBackend.svelte +46 -0
  7. package/dist/components/backends/CreateLocalBackend.svelte.d.ts +7 -0
  8. package/dist/components/backends/OAuthDeviceFlow.svelte +78 -0
  9. package/dist/components/backends/OAuthDeviceFlow.svelte.d.ts +9 -0
  10. package/dist/components/backups/BackupItem.svelte +59 -0
  11. package/dist/components/backups/BackupItem.svelte.d.ts +7 -0
  12. package/dist/components/backups/BackupsList.svelte +82 -0
  13. package/dist/components/backups/BackupsList.svelte.d.ts +8 -0
  14. package/dist/components/backups/dialogs/ConfigureRepositoryModal.svelte +102 -0
  15. package/dist/components/backups/dialogs/ConfigureRepositoryModal.svelte.d.ts +8 -0
  16. package/dist/components/backups/dialogs/CreateRepositoryModal.svelte +59 -0
  17. package/dist/components/backups/dialogs/CreateRepositoryModal.svelte.d.ts +6 -0
  18. package/dist/components/backups/dialogs/ImportRepositoryModal.svelte +62 -0
  19. package/dist/components/backups/dialogs/ImportRepositoryModal.svelte.d.ts +8 -0
  20. package/dist/components/backups/dialogs/RestoreSnapshotModal.svelte +109 -0
  21. package/dist/components/backups/dialogs/RestoreSnapshotModal.svelte.d.ts +8 -0
  22. package/dist/components/backups/dialogs/ViewLogModal.svelte +208 -0
  23. package/dist/components/backups/dialogs/ViewLogModal.svelte.d.ts +7 -0
  24. package/dist/components/backups/metrics-history/MetricsHistoryModal.svelte +166 -0
  25. package/dist/components/backups/metrics-history/MetricsHistoryModal.svelte.d.ts +8 -0
  26. package/dist/components/backups/run-history/RepositoryRunHistory.svelte +39 -0
  27. package/dist/components/backups/run-history/RepositoryRunHistory.svelte.d.ts +7 -0
  28. package/dist/components/backups/run-history/RepositoryRunHistoryItem.svelte +45 -0
  29. package/dist/components/backups/run-history/RepositoryRunHistoryItem.svelte.d.ts +7 -0
  30. package/dist/components/backups/run-history/RunHistoryModal.svelte +18 -0
  31. package/dist/components/backups/run-history/RunHistoryModal.svelte.d.ts +8 -0
  32. package/dist/components/backups/snapshots-list/RepositorySnapshotsList.svelte +53 -0
  33. package/dist/components/backups/snapshots-list/RepositorySnapshotsList.svelte.d.ts +7 -0
  34. package/dist/components/backups/snapshots-list/RepositorySnapshotsListItem.svelte +41 -0
  35. package/dist/components/backups/snapshots-list/RepositorySnapshotsListItem.svelte.d.ts +8 -0
  36. package/dist/components/backups/snapshots-list/SnapshotsListModal.svelte +18 -0
  37. package/dist/components/backups/snapshots-list/SnapshotsListModal.svelte.d.ts +8 -0
  38. package/dist/components/dashboard/Dashboard.svelte +52 -0
  39. package/dist/components/dashboard/Dashboard.svelte.d.ts +9 -0
  40. package/dist/components/dashboard/DashboardAvgBackupTime.svelte +34 -0
  41. package/dist/components/dashboard/DashboardAvgBackupTime.svelte.d.ts +7 -0
  42. package/dist/components/dashboard/DashboardBackupHealth.svelte +91 -0
  43. package/dist/components/dashboard/DashboardBackupHealth.svelte.d.ts +9 -0
  44. package/dist/components/dashboard/DashboardCurrentUsage.svelte +10 -0
  45. package/dist/components/dashboard/DashboardCurrentUsage.svelte.d.ts +18 -0
  46. package/dist/components/dashboard/DashboardDailyBackupTime.svelte +31 -0
  47. package/dist/components/dashboard/DashboardDailyBackupTime.svelte.d.ts +7 -0
  48. package/dist/components/dashboard/DashboardInstall.svelte +15 -0
  49. package/dist/components/dashboard/DashboardInstall.svelte.d.ts +18 -0
  50. package/dist/components/dashboard/DashboardRecentBackups.svelte +104 -0
  51. package/dist/components/dashboard/DashboardRecentBackups.svelte.d.ts +8 -0
  52. package/dist/components/dashboard/DashboardTotalStored.svelte +27 -0
  53. package/dist/components/dashboard/DashboardTotalStored.svelte.d.ts +7 -0
  54. package/dist/components/integrations/immich/ImmichBackupsPage.svelte +14 -0
  55. package/dist/components/integrations/immich/ImmichBackupsPage.svelte.d.ts +6 -0
  56. package/dist/components/integrations/immich/ImmichConfigureBackup.svelte +402 -0
  57. package/dist/components/integrations/immich/ImmichConfigureBackup.svelte.d.ts +9 -0
  58. package/dist/components/integrations/immich/ImmichConfirmDefaultBackup.svelte +80 -0
  59. package/dist/components/integrations/immich/ImmichConfirmDefaultBackup.svelte.d.ts +8 -0
  60. package/dist/components/integrations/immich/ImmichManageBackup.svelte +77 -0
  61. package/dist/components/integrations/immich/ImmichManageBackup.svelte.d.ts +3 -0
  62. package/dist/components/integrations/immich/ImmichManageBackupOverview.svelte +100 -0
  63. package/dist/components/integrations/immich/ImmichManageBackupOverview.svelte.d.ts +8 -0
  64. package/dist/components/integrations/immich/ImmichOnboardingRestoreFlow.svelte +75 -0
  65. package/dist/components/integrations/immich/ImmichOnboardingRestoreFlow.svelte.d.ts +7 -0
  66. package/dist/components/integrations/immich/ImmichOnboardingSetupFlow.svelte +113 -0
  67. package/dist/components/integrations/immich/ImmichOnboardingSetupFlow.svelte.d.ts +8 -0
  68. package/dist/components/onboarding/OnboardingGate.svelte +48 -0
  69. package/dist/components/onboarding/OnboardingGate.svelte.d.ts +9 -0
  70. package/dist/components/onboarding/RecoveryKeyDisplay.svelte +103 -0
  71. package/dist/components/onboarding/RecoveryKeyDisplay.svelte.d.ts +6 -0
  72. package/dist/components/onboarding/SampleOnboarding.svelte +98 -0
  73. package/dist/components/onboarding/SampleOnboarding.svelte.d.ts +9 -0
  74. package/dist/components/onboarding/dialogs/BackupsRecoveryKeyModal.svelte +43 -0
  75. package/dist/components/onboarding/dialogs/BackupsRecoveryKeyModal.svelte.d.ts +6 -0
  76. package/dist/components/onboarding/restore-point-flow/RestorePointFlow.svelte +96 -0
  77. package/dist/components/onboarding/restore-point-flow/RestorePointFlow.svelte.d.ts +8 -0
  78. package/dist/components/onboarding/restore-point-flow/RestorePointFlow2SelectSnapshot.svelte +83 -0
  79. package/dist/components/onboarding/restore-point-flow/RestorePointFlow2SelectSnapshot.svelte.d.ts +9 -0
  80. package/dist/components/onboarding/restore-point-flow/RestorePointFlow3ConfirmRestore.svelte +118 -0
  81. package/dist/components/onboarding/restore-point-flow/RestorePointFlow3ConfirmRestore.svelte.d.ts +10 -0
  82. package/dist/components/onboarding/restore-point-flow/RestorePointFlow4Restore.svelte +59 -0
  83. package/dist/components/onboarding/restore-point-flow/RestorePointFlow4Restore.svelte.d.ts +8 -0
  84. package/dist/components/onboarding/stages/OnboardingStageBackupServices.svelte +82 -0
  85. package/dist/components/onboarding/stages/OnboardingStageBackupServices.svelte.d.ts +8 -0
  86. package/dist/components/onboarding/stages/OnboardingStageKeyConfirm.svelte +56 -0
  87. package/dist/components/onboarding/stages/OnboardingStageKeyConfirm.svelte.d.ts +9 -0
  88. package/dist/components/onboarding/stages/OnboardingStageKeyImport.svelte +57 -0
  89. package/dist/components/onboarding/stages/OnboardingStageKeyImport.svelte.d.ts +8 -0
  90. package/dist/components/onboarding/stages/OnboardingStageKeyIntro.svelte +50 -0
  91. package/dist/components/onboarding/stages/OnboardingStageKeyIntro.svelte.d.ts +7 -0
  92. package/dist/components/onboarding/stages/OnboardingStageKeySave.svelte +44 -0
  93. package/dist/components/onboarding/stages/OnboardingStageKeySave.svelte.d.ts +8 -0
  94. package/dist/components/onboarding/stages/OnboardingStageWelcome.svelte +56 -0
  95. package/dist/components/onboarding/stages/OnboardingStageWelcome.svelte.d.ts +9 -0
  96. package/dist/components/onboarding/stages/SampleCreateFirstBackup.svelte +43 -0
  97. package/dist/components/onboarding/stages/SampleCreateFirstBackup.svelte.d.ts +7 -0
  98. package/dist/components/onboarding/stages/SampleCreateFirstSchedule.svelte +49 -0
  99. package/dist/components/onboarding/stages/SampleCreateFirstSchedule.svelte.d.ts +7 -0
  100. package/dist/components/schedules/RepositoryPicker.svelte +105 -0
  101. package/dist/components/schedules/RepositoryPicker.svelte.d.ts +6 -0
  102. package/dist/components/schedules/ScheduleItem.svelte +47 -0
  103. package/dist/components/schedules/ScheduleItem.svelte.d.ts +8 -0
  104. package/dist/components/schedules/ScheduleList.svelte +51 -0
  105. package/dist/components/schedules/ScheduleList.svelte.d.ts +3 -0
  106. package/dist/components/schedules/dialogs/ConfigureScheduleModal.svelte +48 -0
  107. package/dist/components/schedules/dialogs/ConfigureScheduleModal.svelte.d.ts +8 -0
  108. package/dist/components/schedules/dialogs/CreateScheduleModal.svelte +43 -0
  109. package/dist/components/schedules/dialogs/CreateScheduleModal.svelte.d.ts +6 -0
  110. package/dist/components/test/ImmichTestUi.svelte +183 -0
  111. package/dist/components/test/ImmichTestUi.svelte.d.ts +6 -0
  112. package/dist/components/test/TestUi.svelte +134 -0
  113. package/dist/components/test/TestUi.svelte.d.ts +6 -0
  114. package/dist/components/test/dashboard/ActiveJobs.svelte +380 -0
  115. package/dist/components/test/dashboard/ActiveJobs.svelte.d.ts +3 -0
  116. package/dist/components/test/dashboard/BackupHealth.svelte +95 -0
  117. package/dist/components/test/dashboard/BackupHealth.svelte.d.ts +7 -0
  118. package/dist/components/test/dashboard/BackupStats.svelte +117 -0
  119. package/dist/components/test/dashboard/BackupStats.svelte.d.ts +8 -0
  120. package/dist/components/test/dashboard/Dashboard.svelte +76 -0
  121. package/dist/components/test/dashboard/Dashboard.svelte.d.ts +6 -0
  122. package/dist/components/test/dashboard/RecentBackups.svelte +96 -0
  123. package/dist/components/test/dashboard/RecentBackups.svelte.d.ts +8 -0
  124. package/dist/components/ui/PageLayout.svelte +67 -0
  125. package/dist/components/ui/PageLayout.svelte.d.ts +10 -0
  126. package/dist/components/ui/PathListField.svelte +83 -0
  127. package/dist/components/ui/PathListField.svelte.d.ts +17 -0
  128. package/dist/components/ui/PathPickerField.svelte +74 -0
  129. package/dist/components/ui/PathPickerField.svelte.d.ts +15 -0
  130. package/dist/components/ui/PathPickerModal.svelte +219 -0
  131. package/dist/components/ui/PathPickerModal.svelte.d.ts +14 -0
  132. package/dist/components/ui/StackList.svelte +30 -0
  133. package/dist/components/ui/StackList.svelte.d.ts +30 -0
  134. package/dist/components/ui/StackListItem.svelte +64 -0
  135. package/dist/components/ui/StackListItem.svelte.d.ts +13 -0
  136. package/dist/components/ui/VisualisationGauge.svelte +25 -0
  137. package/dist/components/ui/VisualisationGauge.svelte.d.ts +10 -0
  138. package/dist/components/ui/VisualisationSegmentedBar.svelte +48 -0
  139. package/dist/components/ui/VisualisationSegmentedBar.svelte.d.ts +14 -0
  140. package/dist/components/util/OnEvents.svelte +31 -0
  141. package/dist/components/util/OnEvents.svelte.d.ts +7 -0
  142. package/dist/components/util/RelativeTime.svelte +21 -0
  143. package/dist/components/util/RelativeTime.svelte.d.ts +6 -0
  144. package/dist/components/util/Suspense.svelte +21 -0
  145. package/dist/components/util/Suspense.svelte.d.ts +29 -0
  146. package/dist/components/util/TimedButton.svelte +37 -0
  147. package/dist/components/util/TimedButton.svelte.d.ts +7 -0
  148. package/dist/components/util/YuccaContext.svelte +26 -0
  149. package/dist/components/util/YuccaContext.svelte.d.ts +8 -0
  150. package/dist/events.d.ts +6 -0
  151. package/dist/events.js +47 -0
  152. package/dist/fetch-client.d.ts +289 -0
  153. package/dist/fetch-client.js +233 -0
  154. package/dist/index.d.ts +28 -0
  155. package/dist/index.js +28 -0
  156. package/dist/options.d.ts +5 -0
  157. package/dist/options.js +6 -0
  158. package/dist/providers.d.ts +11 -0
  159. package/dist/providers.js +35 -0
  160. package/dist/query-client.d.ts +2 -0
  161. package/dist/query-client.js +2 -0
  162. package/dist/services/backend.service.d.ts +18 -0
  163. package/dist/services/backend.service.js +61 -0
  164. package/dist/services/filesystem.service.d.ts +2 -0
  165. package/dist/services/filesystem.service.js +11 -0
  166. package/dist/services/immich.integration.service.d.ts +6 -0
  167. package/dist/services/immich.integration.service.js +24 -0
  168. package/dist/services/integrations.service.d.ts +13 -0
  169. package/dist/services/integrations.service.js +42 -0
  170. package/dist/services/log.service.svelte.d.ts +53 -0
  171. package/dist/services/log.service.svelte.js +93 -0
  172. package/dist/services/metricsHistory.service.d.ts +4 -0
  173. package/dist/services/metricsHistory.service.js +12 -0
  174. package/dist/services/onboarding.service.d.ts +11 -0
  175. package/dist/services/onboarding.service.js +56 -0
  176. package/dist/services/repository.service.d.ts +45 -0
  177. package/dist/services/repository.service.js +157 -0
  178. package/dist/services/runHistory.service.d.ts +26 -0
  179. package/dist/services/runHistory.service.js +54 -0
  180. package/dist/services/schedule.service.d.ts +35 -0
  181. package/dist/services/schedule.service.js +126 -0
  182. package/dist/services/snapshot.service.d.ts +29 -0
  183. package/dist/services/snapshot.service.js +108 -0
  184. package/dist/services/task.service.d.ts +3 -0
  185. package/dist/services/task.service.js +20 -0
  186. package/dist/utils/actions.d.ts +2 -0
  187. package/dist/utils/actions.js +3 -0
  188. package/dist/utils/format.d.ts +2 -0
  189. package/dist/utils/format.js +24 -0
  190. package/dist/utils/handle-error.d.ts +9 -0
  191. package/dist/utils/handle-error.js +42 -0
  192. package/package.json +79 -0
@@ -0,0 +1,134 @@
1
+ <script lang="ts">
2
+ import {
3
+ MockProvider,
4
+ orchestrationApiProvider,
5
+ setProvider,
6
+ } from "../../providers";
7
+ import {
8
+ AppShell,
9
+ AppShellHeader,
10
+ AppShellSidebar,
11
+ Heading,
12
+ NavbarItem,
13
+ Text,
14
+ ThemeSwitcher,
15
+ } from "@immich/ui";
16
+ import {
17
+ mdiBackupRestore,
18
+ mdiClock,
19
+ mdiCog,
20
+ mdiViewDashboard,
21
+ } from "@mdi/js";
22
+ import BackendsList from "../backends/BackendsList.svelte";
23
+ import BackupsList from "../backups/BackupsList.svelte";
24
+ import Dashboard from "../dashboard/Dashboard.svelte";
25
+ import ScheduleList from "../schedules/ScheduleList.svelte";
26
+
27
+ const { mock }: { mock: boolean } = $props();
28
+
29
+ // svelte-ignore state_referenced_locally
30
+ if (mock) {
31
+ setProvider(new MockProvider());
32
+ } else {
33
+ setProvider(orchestrationApiProvider);
34
+ }
35
+
36
+ let open = $state(true);
37
+ let route = $state("dashboard");
38
+ </script>
39
+
40
+ <AppShell>
41
+ <AppShellHeader>
42
+ <div class="flex items-center justify-between w-full px-4 py-2">
43
+ <Heading>
44
+ {#if mock}
45
+ <Text class="h-12 font-bold">FUTO Backups</Text>
46
+ {:else}
47
+ <img
48
+ alt="App Name Here"
49
+ src="/app-name-here.png"
50
+ class="inline h-12"
51
+ />
52
+ {/if}
53
+ </Heading>
54
+ <ThemeSwitcher size="medium" color="secondary" />
55
+ </div>
56
+ </AppShellHeader>
57
+
58
+ <AppShellSidebar bind:open>
59
+ <div class="pt-4 pr-2">
60
+ <div
61
+ onclick={() => (route = "dashboard")}
62
+ onkeydown={() => (route = "dashboard")}
63
+ tabindex={0}
64
+ role="button"
65
+ aria-label="Dashboard"
66
+ >
67
+ <NavbarItem
68
+ href="#"
69
+ title="Dashboard"
70
+ icon={mdiViewDashboard}
71
+ active={route === "dashboard"}
72
+ />
73
+ </div>
74
+ <div
75
+ onclick={() => (route = "backups")}
76
+ onkeydown={() => (route = "backups")}
77
+ tabindex={0}
78
+ role="button"
79
+ aria-label="Backups"
80
+ >
81
+ <NavbarItem
82
+ href="#"
83
+ title="Backups"
84
+ icon={mdiBackupRestore}
85
+ active={route === "backups"}
86
+ />
87
+ </div>
88
+ <div
89
+ onclick={() => (route = "schedules")}
90
+ onkeydown={() => (route = "schedules")}
91
+ tabindex={0}
92
+ role="button"
93
+ aria-label="Schedules"
94
+ >
95
+ <NavbarItem
96
+ href="#"
97
+ title="Schedules"
98
+ icon={mdiClock}
99
+ active={route === "schedules"}
100
+ />
101
+ </div>
102
+ <div
103
+ onclick={() => (route = "config")}
104
+ onkeydown={() => (route = "config")}
105
+ tabindex={0}
106
+ role="button"
107
+ aria-label="Configure"
108
+ >
109
+ <NavbarItem
110
+ href="#"
111
+ title="Configure"
112
+ icon={mdiCog}
113
+ active={route === "config"}
114
+ />
115
+ </div>
116
+ </div>
117
+ </AppShellSidebar>
118
+
119
+ <div class="p-4 flex flex-col gap-2 max-w-6xl m-auto">
120
+ {#if route === "dashboard"}
121
+ <Dashboard local onNavigate={(target) => (route = target)} />
122
+ {:else if route === "backups"}
123
+ <BackupsList local />
124
+ {:else if route === "config"}
125
+ {#if !mock}
126
+ <BackendsList />
127
+ {/if}
128
+ {:else if route === "schedules"}
129
+ {#if !mock}
130
+ <ScheduleList />
131
+ {/if}
132
+ {/if}
133
+ </div>
134
+ </AppShell>
@@ -0,0 +1,6 @@
1
+ type $$ComponentProps = {
2
+ mock: boolean;
3
+ };
4
+ declare const TestUi: import("svelte").Component<$$ComponentProps, {}, "">;
5
+ type TestUi = ReturnType<typeof TestUi>;
6
+ export default TestUi;
@@ -0,0 +1,380 @@
1
+ <script lang="ts">
2
+ import type { SocketEvent } from "../../../events";
3
+ import type { RunningTaskDto } from "../../../fetch-client";
4
+ import {
5
+ handleCancelTask,
6
+ handleGetRunningTasks,
7
+ } from "../../../services/task.service";
8
+ import { useRepositories } from "../../../services/repository.service";
9
+ import { useSchedules } from "../../../services/schedule.service";
10
+ import {
11
+ createLogObserver,
12
+ type LogStatus,
13
+ } from "../../../services/log.service.svelte";
14
+ import {
15
+ Badge,
16
+ Button,
17
+ Card,
18
+ CardBody,
19
+ HStack,
20
+ Icon,
21
+ modalManager,
22
+ ProgressBar,
23
+ Text,
24
+ } from "@immich/ui";
25
+ import {
26
+ mdiAlertCircleOutline,
27
+ mdiCheckCircleOutline,
28
+ mdiClockOutline,
29
+ mdiLoading,
30
+ mdiStopCircleOutline,
31
+ mdiTextBoxOutline,
32
+ } from "@mdi/js";
33
+ import { onDestroy, onMount } from "svelte";
34
+ import { SvelteMap } from "svelte/reactivity";
35
+ import OnEvents from "../../util/OnEvents.svelte";
36
+ import RelativeTime from "../../util/RelativeTime.svelte";
37
+ import ViewLogModal from "../../backups/dialogs/ViewLogModal.svelte";
38
+
39
+ const LINGER_MS = 3000;
40
+ const FADE_MS = 600;
41
+
42
+ type LiveTask = RunningTaskDto & {
43
+ startedAt: string;
44
+ completedAt?: string;
45
+ fading?: boolean;
46
+ };
47
+
48
+ let tasks = new SvelteMap<string, LiveTask>();
49
+
50
+ const repositoriesQuery = useRepositories();
51
+ const schedulesQuery = useSchedules();
52
+
53
+ const logObservers = new Map<string, ReturnType<typeof createLogObserver>>();
54
+ const timers = new Map<string, ReturnType<typeof setTimeout>[]>();
55
+
56
+ const getRepoName = (id: string) =>
57
+ repositoriesQuery.data?.find((repository) => repository.id === id)?.name ??
58
+ id;
59
+ const getScheduleName = (id: string) =>
60
+ schedulesQuery.data?.find((schedule) => schedule.id === id)?.name ?? id;
61
+
62
+ const ensureLogObserver = (logId: string) => {
63
+ if (!logObservers.has(logId)) {
64
+ logObservers.set(logId, createLogObserver(logId));
65
+ }
66
+
67
+ return logObservers.get(logId)!;
68
+ };
69
+
70
+ $effect(() => {
71
+ for (const task of tasks.values()) {
72
+ if (task.logId && !task.completedAt) {
73
+ ensureLogObserver(task.logId);
74
+ }
75
+ }
76
+ });
77
+
78
+ const getLogStatus = (logId?: string): LogStatus | undefined => {
79
+ if (!logId) return undefined;
80
+ return logObservers.get(logId)?.status;
81
+ };
82
+
83
+ const taskName = (task: LiveTask) =>
84
+ task.type === "schedule"
85
+ ? getScheduleName(task.parentId)
86
+ : getRepoName(task.parentId);
87
+
88
+ const hasFailedItems = (task: LiveTask) =>
89
+ task.scheduleStatus?.some((item) => item.status === "failed") ?? false;
90
+
91
+ const statusColor = (task: LiveTask) =>
92
+ !task.completedAt
93
+ ? "var(--immich-ui-warning-500)"
94
+ : hasFailedItems(task)
95
+ ? "var(--immich-ui-danger-500)"
96
+ : "var(--immich-ui-success-500)";
97
+
98
+ onMount(async () => {
99
+ const taskData = await handleGetRunningTasks();
100
+
101
+ const now = new Date().toISOString();
102
+ for (const task of taskData.tasks) {
103
+ tasks.set(task.parentId, { ...task, startedAt: now });
104
+ }
105
+ });
106
+
107
+ onDestroy(() => {
108
+ for (const observer of logObservers.values()) {
109
+ observer.destroy();
110
+ }
111
+ for (const bucket of timers.values()) {
112
+ for (const timer of bucket) clearTimeout(timer);
113
+ }
114
+ });
115
+
116
+ const onTaskStart = (event: SocketEvent<{ task: RunningTaskDto }>) => {
117
+ const task = event.data.task;
118
+ tasks.set(task.parentId, {
119
+ ...task,
120
+ startedAt: new Date().toISOString(),
121
+ });
122
+ };
123
+
124
+ const onTaskUpdate = (
125
+ event: SocketEvent<{ parentId: string; task: Partial<RunningTaskDto> }>,
126
+ ) => {
127
+ const existing = tasks.get(event.data.parentId);
128
+ if (existing) {
129
+ tasks.set(event.data.parentId, { ...existing, ...event.data.task });
130
+ }
131
+ };
132
+
133
+ const onTaskEnd = (event: SocketEvent<{ parentId: string }>) => {
134
+ const id = event.data.parentId;
135
+ const existing = tasks.get(id);
136
+ if (!existing) return;
137
+
138
+ if (existing.logId && logObservers.has(existing.logId)) {
139
+ logObservers.get(existing.logId)!.destroy();
140
+ logObservers.delete(existing.logId);
141
+ }
142
+
143
+ tasks.set(id, { ...existing, completedAt: new Date().toISOString() });
144
+
145
+ const lingerTimer = setTimeout(() => {
146
+ const task = tasks.get(id);
147
+ if (task) {
148
+ tasks.set(id, { ...task, fading: true });
149
+ }
150
+ const fadeTimer = setTimeout(() => tasks.delete(id), FADE_MS);
151
+ bucket.push(fadeTimer);
152
+ }, LINGER_MS);
153
+ const bucket = [lingerTimer];
154
+ timers.set(id, bucket);
155
+ };
156
+
157
+ const shouldDisplay = (task: LiveTask) => {
158
+ if (
159
+ task.type === "backup" &&
160
+ tasks
161
+ .values()
162
+ .some((entry) =>
163
+ entry.scheduleStatus?.some(
164
+ (item) => item.repositoryId === task.parentId,
165
+ ),
166
+ )
167
+ ) {
168
+ return false;
169
+ }
170
+ return true;
171
+ };
172
+
173
+ const openLog = (logId: string) => {
174
+ modalManager.open(ViewLogModal, { logId });
175
+ };
176
+
177
+ const onCancel = (parentId: string) => handleCancelTask(parentId);
178
+ </script>
179
+
180
+ <OnEvents {onTaskStart} {onTaskUpdate} {onTaskEnd} />
181
+
182
+ {#snippet logButton(logId: string | undefined)}
183
+ {#if logId}
184
+ <Button size="tiny" variant="ghost" onclick={() => openLog(logId)}>
185
+ <Icon icon={mdiTextBoxOutline} size="14" />
186
+ Log
187
+ </Button>
188
+ {/if}
189
+ {/snippet}
190
+
191
+ {#snippet scheduleItems(task: LiveTask, compact: boolean)}
192
+ {#if task.scheduleStatus}
193
+ <div class="flex flex-col gap-2 pl-7">
194
+ {#each task.scheduleStatus as item (item.repositoryId)}
195
+ {@const subTask = compact ? undefined : tasks.get(item.repositoryId)}
196
+ {@const subLog =
197
+ !compact && subTask?.logId ? getLogStatus(subTask.logId) : undefined}
198
+ <div class="flex flex-col gap-1">
199
+ <HStack class="items-center justify-between">
200
+ <HStack class="items-center gap-2">
201
+ {#if item.status === "complete"}
202
+ <Icon
203
+ icon={mdiCheckCircleOutline}
204
+ size="16"
205
+ class="text-green-500"
206
+ />
207
+ {:else if item.status === "failed"}
208
+ <Icon
209
+ icon={mdiAlertCircleOutline}
210
+ size="16"
211
+ class="text-danger-500"
212
+ />
213
+ {:else if subLog && subLog.progress > 0}
214
+ <Icon
215
+ icon={mdiLoading}
216
+ size="16"
217
+ class="animate-spin opacity-60"
218
+ />
219
+ {:else}
220
+ <Icon icon={mdiClockOutline} size="16" class="opacity-40" />
221
+ {/if}
222
+ <Text
223
+ size="small"
224
+ color={item.status === "incomplete" &&
225
+ !(subLog && subLog.progress > 0)
226
+ ? "secondary"
227
+ : undefined}
228
+ >
229
+ {getRepoName(item.repositoryId)}
230
+ </Text>
231
+ {#if item.status === "complete"}
232
+ <Badge size="tiny" color="success">Done</Badge>
233
+ {:else if item.status === "failed"}
234
+ <Badge size="tiny" color="danger">Failed</Badge>
235
+ {:else if subLog && subLog.progress > 0}
236
+ <Badge size="tiny" color="warning"
237
+ >{Math.round(subLog.progress * 100)}%</Badge
238
+ >
239
+ {:else}
240
+ <Badge size="tiny">Queued</Badge>
241
+ {/if}
242
+ </HStack>
243
+ {#if !compact}
244
+ <HStack class="items-center gap-2">
245
+ {@render logButton(subTask?.logId)}
246
+ {#if subTask && !subTask.completedAt}
247
+ <Button
248
+ size="tiny"
249
+ variant="ghost"
250
+ color="danger"
251
+ onclick={() => onCancel(item.repositoryId)}
252
+ >
253
+ <Icon icon={mdiStopCircleOutline} size="14" />
254
+ Cancel
255
+ </Button>
256
+ {/if}
257
+ </HStack>
258
+ {/if}
259
+ </HStack>
260
+ {#if !compact && subLog && subLog.progress > 0}
261
+ <div class="pl-6">
262
+ <ProgressBar progress={subLog.progress} size="small" />
263
+ </div>
264
+ {/if}
265
+ </div>
266
+ {/each}
267
+ </div>
268
+ {/if}
269
+ {/snippet}
270
+
271
+ {#each tasks.values() as task (task.parentId)}
272
+ {#if shouldDisplay(task)}
273
+ <div class="task-card" class:task-card-fading={task.fading}>
274
+ <Card style="background: color-mix(in oklch, {statusColor(task)}, transparent 92%);">
275
+ <CardBody class="flex flex-col gap-3">
276
+ <HStack class="items-center justify-between">
277
+ <HStack class="items-center gap-3">
278
+ {#if task.completedAt}
279
+ <Icon
280
+ icon={hasFailedItems(task)
281
+ ? mdiAlertCircleOutline
282
+ : mdiCheckCircleOutline}
283
+ size="18"
284
+ class={hasFailedItems(task)
285
+ ? "text-danger-500"
286
+ : "text-green-500"}
287
+ />
288
+ {:else}
289
+ <Icon
290
+ icon={mdiLoading}
291
+ size="18"
292
+ class="animate-spin opacity-60"
293
+ />
294
+ {/if}
295
+ <Text>
296
+ {#if task.completedAt}
297
+ {#if task.type === "schedule"}Finished schedule{:else if task.type === "forget"}Finished pruning{:else}Finished
298
+ backup{/if}
299
+ {:else if task.type === "schedule"}Running schedule{:else if task.type === "forget"}Pruning{:else}Backing
300
+ up{/if}
301
+ — <strong>{taskName(task)}</strong>
302
+ </Text>
303
+ {#if !task.completedAt && task.type === "backup"}
304
+ {@const log = getLogStatus(task.logId)}
305
+ {#if log && log.progress > 0}
306
+ <Badge size="tiny" color="warning"
307
+ >{Math.round(log.progress * 100)}%</Badge
308
+ >
309
+ {/if}
310
+ {/if}
311
+ </HStack>
312
+ <HStack class="items-center gap-2">
313
+ <Text color="secondary" size="tiny">
314
+ {#if task.completedAt}
315
+ <RelativeTime time={task.completedAt} />
316
+ {:else}
317
+ Started <RelativeTime time={task.startedAt} />
318
+ {/if}
319
+ </Text>
320
+ {@render logButton(task.logId)}
321
+ {#if !task.completedAt}
322
+ <Button
323
+ size="tiny"
324
+ variant="ghost"
325
+ color="danger"
326
+ onclick={() => onCancel(task.parentId)}
327
+ >
328
+ <Icon icon={mdiStopCircleOutline} size="14" />
329
+ Cancel
330
+ </Button>
331
+ {/if}
332
+ </HStack>
333
+ </HStack>
334
+
335
+ {#if task.type === "schedule" && task.scheduleStatus}
336
+ {#if task.completedAt}
337
+ {@render scheduleItems(task, true)}
338
+ {:else}
339
+ {@render scheduleItems(task, false)}
340
+ {/if}
341
+ {/if}
342
+
343
+ {#if !task.completedAt && task.type === "backup"}
344
+ {@const log = getLogStatus(task.logId)}
345
+ {#if log && log.progress > 0}
346
+ <ProgressBar progress={log.progress} size="small" />
347
+ {/if}
348
+ {/if}
349
+ </CardBody>
350
+ </Card>
351
+ </div>
352
+ {/if}
353
+ {/each}
354
+
355
+ <style>
356
+ .task-card {
357
+ opacity: 1;
358
+ transform: translateY(0);
359
+ transition-property: opacity, transform;
360
+ transition-duration: 600ms;
361
+ transition-timing-function: ease-out;
362
+ animation: task-enter 300ms ease-out;
363
+ }
364
+
365
+ .task-card-fading {
366
+ opacity: 0;
367
+ transform: translateY(-8px);
368
+ }
369
+
370
+ @keyframes task-enter {
371
+ from {
372
+ opacity: 0;
373
+ transform: translateY(8px);
374
+ }
375
+ to {
376
+ opacity: 1;
377
+ transform: translateY(0);
378
+ }
379
+ }
380
+ </style>
@@ -0,0 +1,3 @@
1
+ declare const ActiveJobs: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ActiveJobs = ReturnType<typeof ActiveJobs>;
3
+ export default ActiveJobs;
@@ -0,0 +1,95 @@
1
+ <script lang="ts">
2
+ import type { LocalRepositoryDto } from "../../../fetch-client";
3
+ import { Alert, Text } from "@immich/ui";
4
+ import SegmentedBar from "../../ui/VisualisationSegmentedBar.svelte";
5
+
6
+ type Props = {
7
+ repositories: LocalRepositoryDto[];
8
+ };
9
+
10
+ const { repositories }: Props = $props();
11
+
12
+ const total = $derived(repositories.length);
13
+
14
+ const status = $derived(
15
+ repositories.reduce(
16
+ (tally, repo) => {
17
+ if (!repo.backends?.primary.online) {
18
+ tally.offline++;
19
+ } else if (!repo.metrics?.lastBackup) {
20
+ tally.neverRun++;
21
+ } else if (
22
+ repo.metrics.lastBackup === repo.metrics.lastSuccessfulBackup
23
+ ) {
24
+ tally.success++;
25
+ } else {
26
+ tally.failed++;
27
+ }
28
+ return tally;
29
+ },
30
+ { success: 0, offline: 0, failed: 0, neverRun: 0 },
31
+ ),
32
+ );
33
+
34
+ const hasFailures = $derived(status.offline > 0 || status.failed > 0);
35
+ const allHealthy = $derived(
36
+ total > 0 &&
37
+ status.offline === 0 &&
38
+ status.failed === 0 &&
39
+ status.neverRun === 0,
40
+ );
41
+ const allNeverRun = $derived(
42
+ total > 0 &&
43
+ status.success === 0 &&
44
+ status.offline === 0 &&
45
+ status.failed === 0,
46
+ );
47
+ </script>
48
+
49
+ {#if allNeverRun}
50
+ <Alert color="info">
51
+ <Text class="grow">{total} backups configured</Text>
52
+ </Alert>
53
+ {:else if hasFailures}
54
+ <Alert color="danger">
55
+ <Text class="grow"
56
+ >{status.failed + status.offline} backups failed or offline — check the Backups
57
+ page for details</Text
58
+ >
59
+ </Alert>
60
+ {:else if allHealthy}
61
+ <Alert color="success">
62
+ <Text class="grow">All {total} backups healthy</Text>
63
+ </Alert>
64
+ {/if}
65
+
66
+ <SegmentedBar
67
+ title="Backup Health"
68
+ summary="{status.success} of {total} successful"
69
+ segments={[
70
+ {
71
+ value: status.success,
72
+ label: "Successful",
73
+ color: "var(--immich-ui-success-500)",
74
+ badge: "success",
75
+ },
76
+ {
77
+ value: status.offline,
78
+ label: "Offline",
79
+ color: "var(--immich-ui-warning-500)",
80
+ badge: "warning",
81
+ },
82
+ {
83
+ value: status.failed,
84
+ label: "Failed",
85
+ color: "var(--immich-ui-danger-500)",
86
+ badge: "danger",
87
+ },
88
+ {
89
+ value: status.neverRun,
90
+ label: "Never Run",
91
+ color: "var(--immich-ui-light-400)",
92
+ badge: "secondary",
93
+ },
94
+ ]}
95
+ />
@@ -0,0 +1,7 @@
1
+ import type { LocalRepositoryDto } from "../../../fetch-client";
2
+ type Props = {
3
+ repositories: LocalRepositoryDto[];
4
+ };
5
+ declare const BackupHealth: import("svelte").Component<Props, {}, "">;
6
+ type BackupHealth = ReturnType<typeof BackupHealth>;
7
+ export default BackupHealth;