@futo-org/backups-orchestrator-ui 0.43.0 → 0.45.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 (40) hide show
  1. package/dist/components/backups/BackupStatus.svelte +14 -3
  2. package/dist/components/backups/BackupStatus.svelte.d.ts +1 -1
  3. package/dist/components/backups/dialogs/ViewStatusModal.svelte +6 -0
  4. package/dist/components/backups/metrics-history/MetricsHistoryModal.svelte +34 -5
  5. package/dist/components/backups/run-history/RepositoryRunHistoryItem.svelte +10 -3
  6. package/dist/components/dashboard/DashboardBackupHealth.svelte +11 -1
  7. package/dist/components/dashboard/{Dashboard.svelte.d.ts → DashboardPage.svelte.d.ts} +3 -3
  8. package/dist/components/integrations/immich/ImmichBackupAttemptsPage.svelte +19 -0
  9. package/dist/components/integrations/immich/ImmichBackupAttemptsPage.svelte.d.ts +6 -0
  10. package/dist/components/integrations/immich/ImmichBackupSettingsPage.svelte +23 -0
  11. package/dist/components/integrations/immich/ImmichBackupSettingsPage.svelte.d.ts +6 -0
  12. package/dist/components/integrations/immich/ImmichBackupsPage.svelte +13 -3
  13. package/dist/components/integrations/immich/ImmichBackupsPage.svelte.d.ts +3 -0
  14. package/dist/components/integrations/immich/ImmichBackupsSidebarItem.svelte +8 -0
  15. package/dist/components/integrations/immich/ImmichConfiguredGate.svelte +32 -0
  16. package/dist/components/integrations/immich/ImmichConfiguredGate.svelte.d.ts +9 -0
  17. package/dist/components/integrations/immich/ImmichManageBackupOverview.svelte +32 -9
  18. package/dist/components/integrations/immich/ImmichManageBackupPage.svelte +50 -0
  19. package/dist/components/integrations/immich/ImmichManageBackupPage.svelte.d.ts +8 -0
  20. package/dist/components/integrations/immich/ImmichSnapshotsPage.svelte +19 -0
  21. package/dist/components/integrations/immich/ImmichSnapshotsPage.svelte.d.ts +6 -0
  22. package/dist/components/test/ImmichTestUi.svelte +30 -3
  23. package/dist/components/test/TestUi.svelte +2 -2
  24. package/dist/components/ui/VisualisationSegmentedBar.svelte +1 -1
  25. package/dist/components/ui/VisualisationSegmentedBar.svelte.d.ts +1 -1
  26. package/dist/fetch-client.d.ts +9 -4
  27. package/dist/fetch-client.js +5 -0
  28. package/dist/index.d.ts +5 -2
  29. package/dist/index.js +5 -2
  30. package/dist/services/immich.integration.service.d.ts +1 -1
  31. package/dist/services/runHistory.service.d.ts +1 -0
  32. package/dist/services/runHistory.service.js +6 -1
  33. package/dist/utils/backup-status.d.ts +1 -1
  34. package/dist/utils/backup-status.js +6 -0
  35. package/package.json +3 -3
  36. package/dist/components/integrations/immich/ImmichBackupSettings.svelte +0 -12
  37. package/dist/components/integrations/immich/ImmichBackupSettings.svelte.d.ts +0 -18
  38. package/dist/components/integrations/immich/ImmichManageBackup.svelte +0 -76
  39. package/dist/components/integrations/immich/ImmichManageBackup.svelte.d.ts +0 -3
  40. /package/dist/components/dashboard/{Dashboard.svelte → DashboardPage.svelte} +0 -0
@@ -6,7 +6,8 @@
6
6
  | "running"
7
7
  | "complete"
8
8
  | "warned"
9
- | "failed";
9
+ | "failed"
10
+ | "cancelled";
10
11
  </script>
11
12
 
12
13
  <script lang="ts">
@@ -75,6 +76,12 @@
75
76
  forget: "Old backups could not be pruned",
76
77
  };
77
78
 
79
+ const cancelled: Record<BackupStatusType, string> = {
80
+ backup: "Your backup was cancelled",
81
+ restore: "Your restore was cancelled",
82
+ forget: "Pruning old backups was cancelled",
83
+ };
84
+
78
85
  const reassurance: Record<BackupStatusType, string> = {
79
86
  backup: "No changes were made to your existing backups.",
80
87
  restore: "Your backups are untouched — nothing was lost.",
@@ -106,6 +113,9 @@
106
113
  case "failed": {
107
114
  return failed[type];
108
115
  }
116
+ case "cancelled": {
117
+ return cancelled[type];
118
+ }
109
119
  case "complete": {
110
120
  return succeeded[type];
111
121
  }
@@ -121,7 +131,7 @@
121
131
  const titleColor = $derived(
122
132
  backupState === "complete"
123
133
  ? "success"
124
- : backupState === "warned"
134
+ : backupState === "warned" || backupState === "cancelled"
125
135
  ? "warning"
126
136
  : backupState === "failed"
127
137
  ? "danger"
@@ -131,7 +141,8 @@
131
141
  const terminal = $derived(
132
142
  backupState === "complete" ||
133
143
  backupState === "warned" ||
134
- backupState === "failed",
144
+ backupState === "failed" ||
145
+ backupState === "cancelled",
135
146
  );
136
147
  </script>
137
148
 
@@ -1,5 +1,5 @@
1
1
  export type BackupStatusType = "backup" | "restore" | "forget";
2
- export type BackupStatusState = "connecting" | "running" | "complete" | "warned" | "failed";
2
+ export type BackupStatusState = "connecting" | "running" | "complete" | "warned" | "failed" | "cancelled";
3
3
  import type { Snippet } from "svelte";
4
4
  type Props = {
5
5
  title: string;
@@ -64,6 +64,9 @@
64
64
  case "failed": {
65
65
  return "failed";
66
66
  }
67
+ case "cancelled": {
68
+ return "cancelled";
69
+ }
67
70
  case "warn": {
68
71
  return "warned";
69
72
  }
@@ -109,6 +112,9 @@
109
112
  case "failed": {
110
113
  return `${titles[type].done} failed`;
111
114
  }
115
+ case "cancelled": {
116
+ return `${titles[type].done} cancelled`;
117
+ }
112
118
  default: {
113
119
  return titles[type].running;
114
120
  }
@@ -77,10 +77,7 @@
77
77
  {@const isStart = entry.started != null}
78
78
  {@const isEnd = entry.backup != null}
79
79
  {@const isSize = entry.sizeBytes != null && !isStart && !isEnd}
80
- {@const succeeded =
81
- isEnd &&
82
- entry.successfulBackup != null &&
83
- entry.successfulBackup === entry.backup}
80
+ {@const status = entry.backupStatus}
84
81
  <Stack gap={1} class="px-4 py-3">
85
82
  <HStack class="justify-between gap-4">
86
83
  <HStack class="gap-2">
@@ -91,7 +88,7 @@
91
88
  class="text-info-500"
92
89
  />
93
90
  <Text>Backup started</Text>
94
- {:else if isEnd && succeeded}
91
+ {:else if status === 'complete'}
95
92
  <Icon
96
93
  icon={mdiCheckCircleOutline}
97
94
  size="18"
@@ -99,6 +96,38 @@
99
96
  />
100
97
  <Text>Backup finished</Text>
101
98
  <Badge size="tiny" color="success">Success</Badge>
99
+ {:else if status === 'warn'}
100
+ <Icon
101
+ icon={mdiAlertCircleOutline}
102
+ size="18"
103
+ class="text-warning-500"
104
+ />
105
+ <Text>Backup finished</Text>
106
+ <Badge size="tiny" color="warning">Warning</Badge>
107
+ {:else if status === 'incomplete'}
108
+ <Icon
109
+ icon={mdiAlertCircleOutline}
110
+ size="18"
111
+ class="text-warning-500"
112
+ />
113
+ <Text>Backup incomplete</Text>
114
+ <Badge size="tiny" color="warning">Incomplete</Badge>
115
+ {:else if status === 'failed'}
116
+ <Icon
117
+ icon={mdiAlertCircleOutline}
118
+ size="18"
119
+ class="text-danger-500"
120
+ />
121
+ <Text>Backup finished</Text>
122
+ <Badge size="tiny" color="danger">Failed</Badge>
123
+ {:else if status === 'cancelled'}
124
+ <Icon
125
+ icon={mdiAlertCircleOutline}
126
+ size="18"
127
+ class="text-warning-500"
128
+ />
129
+ <Text>Backup cancelled</Text>
130
+ <Badge size="tiny" color="warning">Cancelled</Badge>
102
131
  {:else if isEnd}
103
132
  <Icon
104
133
  icon={mdiAlertCircleOutline}
@@ -15,7 +15,7 @@
15
15
  };
16
16
 
17
17
  const { run }: Props = $props();
18
- const { ViewLog } = $derived(getRunActions(run));
18
+ const { ViewLog, DownloadLog } = $derived(getRunActions(run));
19
19
 
20
20
  const nouns = {
21
21
  restore: { name: "restore", running: "Restore", done: "Restored" },
@@ -38,6 +38,13 @@
38
38
  icon: mdiCloudOffOutline,
39
39
  } as const;
40
40
  }
41
+ case "cancelled": {
42
+ return {
43
+ title: `Cancelled ${noun.name}`,
44
+ color: "warning",
45
+ icon: mdiCloudOffOutline,
46
+ } as const;
47
+ }
41
48
  case "warn": {
42
49
  return {
43
50
  title: `${noun.done} with warnings`,
@@ -63,14 +70,14 @@
63
70
  });
64
71
  </script>
65
72
 
66
- <StackListItem title={status.title} color={status.color} actions={[ViewLog]}>
73
+ <StackListItem title={status.title} color={status.color} actions={[ViewLog, DownloadLog]}>
67
74
  {#snippet icon()}
68
75
  <Icon icon={status.icon} />
69
76
  {/snippet}
70
77
 
71
78
  {#if run.status === "incomplete"}
72
79
  Started <RelativeTime time={run.start} />
73
- {:else if run.status === "failed"}
80
+ {:else if run.status === "failed" || run.status === "cancelled"}
74
81
  Attempted {#if run.end}<RelativeTime time={run.end} />{/if}
75
82
  {:else}
76
83
  {noun.done} {#if run.end}<RelativeTime time={run.end} />{/if}
@@ -44,6 +44,10 @@
44
44
  tally.warned++;
45
45
  break;
46
46
  }
47
+ case "incomplete": {
48
+ tally.incomplete++;
49
+ break;
50
+ }
47
51
  default: {
48
52
  tally.success++;
49
53
  break;
@@ -52,7 +56,7 @@
52
56
 
53
57
  return tally;
54
58
  },
55
- { success: 0, offline: 0, warned: 0, failed: 0, neverRun: 0 },
59
+ { success: 0, offline: 0, warned: 0, failed: 0, incomplete: 0, neverRun: 0 },
56
60
  ),
57
61
  );
58
62
  </script>
@@ -79,6 +83,12 @@
79
83
  color: "var(--immich-ui-success-500)",
80
84
  badge: "success",
81
85
  },
86
+ {
87
+ value: status.incomplete,
88
+ label: "In-progress or incomplete",
89
+ color: "var(--immich-ui-info-400)",
90
+ badge: "info",
91
+ },
82
92
  {
83
93
  value: status.offline,
84
94
  label: "Offline",
@@ -3,6 +3,6 @@ type Props = {
3
3
  initialData?: RepositoryListResponseDto;
4
4
  onViewBackups?: () => void;
5
5
  };
6
- declare const Dashboard: import("svelte").Component<Props, {}, "">;
7
- type Dashboard = ReturnType<typeof Dashboard>;
8
- export default Dashboard;
6
+ declare const DashboardPage: import("svelte").Component<Props, {}, "">;
7
+ type DashboardPage = ReturnType<typeof DashboardPage>;
8
+ export default DashboardPage;
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ import RepositoryRunHistoryPage from "../../backups/run-history/RepositoryRunHistoryPage.svelte";
3
+ import { Container } from "@immich/ui";
4
+ import ImmichConfiguredGate from "./ImmichConfiguredGate.svelte";
5
+
6
+ type Props = {
7
+ onUnconfigured: () => void;
8
+ };
9
+
10
+ const { onUnconfigured }: Props = $props();
11
+ </script>
12
+
13
+ <ImmichConfiguredGate {onUnconfigured}>
14
+ {#snippet children(repository)}
15
+ <Container size="medium" center>
16
+ <RepositoryRunHistoryPage {repository} />
17
+ </Container>
18
+ {/snippet}
19
+ </ImmichConfiguredGate>
@@ -0,0 +1,6 @@
1
+ type Props = {
2
+ onUnconfigured: () => void;
3
+ };
4
+ declare const ImmichBackupAttemptsPage: import("svelte").Component<Props, {}, "">;
5
+ type ImmichBackupAttemptsPage = ReturnType<typeof ImmichBackupAttemptsPage>;
6
+ export default ImmichBackupAttemptsPage;
@@ -0,0 +1,23 @@
1
+ <script lang="ts">
2
+ import { Container } from "@immich/ui";
3
+ import ImmichConfiguredGate from "./ImmichConfiguredGate.svelte";
4
+ import ImmichSettingsBackupContents from "./settings/ImmichSettingsBackupContents.svelte";
5
+ import ImmichSettingsSchedule from "./settings/ImmichSettingsSchedule.svelte";
6
+ import ImmichSettingsStorage from "./settings/ImmichSettingsStorage.svelte";
7
+
8
+ type Props = {
9
+ onUnconfigured: () => void;
10
+ };
11
+
12
+ const { onUnconfigured }: Props = $props();
13
+ </script>
14
+
15
+ <ImmichConfiguredGate {onUnconfigured}>
16
+ {#snippet children()}
17
+ <Container size="large" center>
18
+ <ImmichSettingsBackupContents />
19
+ <ImmichSettingsSchedule />
20
+ <ImmichSettingsStorage />
21
+ </Container>
22
+ {/snippet}
23
+ </ImmichConfiguredGate>
@@ -0,0 +1,6 @@
1
+ type Props = {
2
+ onUnconfigured: () => void;
3
+ };
4
+ declare const ImmichBackupSettingsPage: import("svelte").Component<Props, {}, "">;
5
+ type ImmichBackupSettingsPage = ReturnType<typeof ImmichBackupSettingsPage>;
6
+ export default ImmichBackupSettingsPage;
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
2
  import type { Snippet } from "svelte";
3
3
  import UpsellPage from "../../onboarding/upsell/UpsellPage.svelte";
4
- import ImmichManageBackup from "./ImmichManageBackup.svelte";
4
+ import ImmichManageBackupPage from "./ImmichManageBackupPage.svelte";
5
5
  import ImmichOnboardingSetupFlow from "./ImmichOnboardingSetupFlow.svelte";
6
6
 
7
7
  type Question = {
@@ -13,9 +13,19 @@
13
13
  price: string;
14
14
  includedStorage: string;
15
15
  questions: Question[];
16
+ onConfigure: () => void;
17
+ onViewAttempts: () => void;
18
+ onViewSnapshots: () => void;
16
19
  };
17
20
 
18
- const { price, includedStorage, questions }: Props = $props();
21
+ const {
22
+ price,
23
+ includedStorage,
24
+ questions,
25
+ onConfigure,
26
+ onViewAttempts,
27
+ onViewSnapshots,
28
+ }: Props = $props();
19
29
  </script>
20
30
 
21
31
  <ImmichOnboardingSetupFlow>
@@ -23,5 +33,5 @@
23
33
  <UpsellPage {price} {includedStorage} {questions} onGetStarted={onStart} />
24
34
  {/snippet}
25
35
 
26
- <ImmichManageBackup />
36
+ <ImmichManageBackupPage {onConfigure} {onViewAttempts} {onViewSnapshots} />
27
37
  </ImmichOnboardingSetupFlow>
@@ -7,6 +7,9 @@ type Props = {
7
7
  price: string;
8
8
  includedStorage: string;
9
9
  questions: Question[];
10
+ onConfigure: () => void;
11
+ onViewAttempts: () => void;
12
+ onViewSnapshots: () => void;
10
13
  };
11
14
  declare const ImmichBackupsPage: import("svelte").Component<Props, {}, "">;
12
15
  type ImmichBackupsPage = ReturnType<typeof ImmichBackupsPage>;
@@ -44,6 +44,10 @@
44
44
  case "warn": {
45
45
  return { color: "warning", icon: mdiCloudCheckVariantOutline } as const;
46
46
  }
47
+ case "incomplete":
48
+ case "cancelled": {
49
+ return { color: "warning", icon: mdiCloudAlertOutline } as const;
50
+ }
47
51
  case "paused": {
48
52
  return { color: "warning", icon: mdiCloudAlertOutline } as const;
49
53
  }
@@ -93,6 +97,10 @@
93
97
  Backup failed
94
98
  {:else if status.kind === "warn"}
95
99
  Backed up with warnings <RelativeTime time={status.lastBackup} />
100
+ {:else if status.kind === "incomplete"}
101
+ Backup did not complete <RelativeTime time={status.lastBackup} />
102
+ {:else if status.kind === "cancelled"}
103
+ Backup was cancelled <RelativeTime time={status.lastBackup} />
96
104
  {:else if status.kind === "paused"}
97
105
  Backups paused
98
106
  {:else if status.kind === "unconfigured"}
@@ -0,0 +1,32 @@
1
+ <script lang="ts">
2
+ import OnEvents from "../../util/OnEvents.svelte";
3
+ import type { LocalRepositoryDto } from "../../../fetch-client";
4
+ import {
5
+ useImmichBackupStatus,
6
+ useImmichBackupStatusEventHandler,
7
+ } from "../../../services/immich.integration.service";
8
+ import type { Snippet } from "svelte";
9
+
10
+ type Props = {
11
+ children: Snippet<[LocalRepositoryDto]>;
12
+ onUnconfigured: () => void;
13
+ };
14
+
15
+ const { children, onUnconfigured }: Props = $props();
16
+
17
+ const backup = useImmichBackupStatus();
18
+
19
+ const { repository, status } = $derived(backup);
20
+
21
+ $effect(() => {
22
+ if (status.kind === "unconfigured") {
23
+ onUnconfigured();
24
+ }
25
+ });
26
+ </script>
27
+
28
+ <OnEvents {...useImmichBackupStatusEventHandler()} />
29
+
30
+ {#if repository}
31
+ {@render children(repository)}
32
+ {/if}
@@ -0,0 +1,9 @@
1
+ import type { LocalRepositoryDto } from "../../../fetch-client";
2
+ import type { Snippet } from "svelte";
3
+ type Props = {
4
+ children: Snippet<[LocalRepositoryDto]>;
5
+ onUnconfigured: () => void;
6
+ };
7
+ declare const ImmichConfiguredGate: import("svelte").Component<Props, {}, "">;
8
+ type ImmichConfiguredGate = ReturnType<typeof ImmichConfiguredGate>;
9
+ export default ImmichConfiguredGate;
@@ -5,6 +5,7 @@
5
5
  import type { LocalRepositoryDto, ScheduleDto } from "../../../fetch-client";
6
6
  import type { ImmichBackupStatus } from "../../../services/immich.integration.service";
7
7
  import { handleCreateBackup } from "../../../services/repository.service";
8
+ import { handleCancelTask } from "../../../services/task.service";
8
9
  import { Button, FormatBytes, Icon } from "@immich/ui";
9
10
  import {
10
11
  mdiAlert,
@@ -13,6 +14,7 @@
13
14
  mdiCloudUploadOutline,
14
15
  mdiInformation,
15
16
  mdiProgressUpload,
17
+ mdiStopCircleOutline,
16
18
  } from "@mdi/js";
17
19
  import cronstrue from "cronstrue";
18
20
 
@@ -37,6 +39,10 @@
37
39
  case "warn": {
38
40
  return { color: "warning", icon: mdiAlert } as const;
39
41
  }
42
+ case "incomplete":
43
+ case "cancelled": {
44
+ return { color: "warning", icon: mdiAlert } as const;
45
+ }
40
46
  case "complete": {
41
47
  return { color: "success", icon: mdiCheck } as const;
42
48
  }
@@ -64,15 +70,28 @@
64
70
  </span>
65
71
 
66
72
  {#snippet trailing()}
67
- <Button
68
- variant="ghost"
69
- size="small"
70
- class="whitespace-nowrap"
71
- leadingIcon={mdiCloudUploadOutline}
72
- onclick={() => void handleCreateBackup(repository.id)}
73
- >
74
- Back up now
75
- </Button>
73
+ {#if status.kind === "running"}
74
+ <Button
75
+ variant="ghost"
76
+ size="small"
77
+ color="danger"
78
+ class="whitespace-nowrap"
79
+ leadingIcon={mdiStopCircleOutline}
80
+ onclick={() => void handleCancelTask(repository.id)}
81
+ >
82
+ Cancel backup
83
+ </Button>
84
+ {:else}
85
+ <Button
86
+ variant="ghost"
87
+ size="small"
88
+ class="whitespace-nowrap"
89
+ leadingIcon={mdiCloudUploadOutline}
90
+ onclick={() => void handleCreateBackup(repository.id)}
91
+ >
92
+ Back up now
93
+ </Button>
94
+ {/if}
76
95
  {/snippet}
77
96
 
78
97
  {#snippet footer()}
@@ -88,6 +107,10 @@
88
107
  Last backup failed <RelativeTime time={status.lastBackup} />
89
108
  {:else if status.kind === "warn"}
90
109
  Last backup finished with warnings <RelativeTime time={status.lastBackup} />
110
+ {:else if status.kind === "incomplete"}
111
+ Last backup did not complete <RelativeTime time={status.lastBackup} />
112
+ {:else if status.kind === "cancelled"}
113
+ Last backup was cancelled <RelativeTime time={status.lastBackup} />
91
114
  {:else if status.kind === "complete"}
92
115
  Last backup successful <RelativeTime time={status.lastBackup} />
93
116
  {:else if status.kind === "paused"}
@@ -0,0 +1,50 @@
1
+ <script lang="ts">
2
+ import BackendsList from "../../backends/BackendsList.svelte";
3
+ import RepositoryRunHistory from "../../backups/run-history/RepositoryRunHistory.svelte";
4
+ import RepositorySnapshotsList from "../../backups/snapshots-list/RepositorySnapshotsList.svelte";
5
+ import PageLayout from "../../ui/PageLayout.svelte";
6
+ import OnEvents from "../../util/OnEvents.svelte";
7
+ import {
8
+ getBackupPageActions,
9
+ useImmichBackupStatus,
10
+ useImmichBackupStatusEventHandler,
11
+ } from "../../../services/immich.integration.service";
12
+ import { Container, Stack } from "@immich/ui";
13
+ import ImmichManageBackupOverview from "./ImmichManageBackupOverview.svelte";
14
+
15
+ type Props = {
16
+ onConfigure: () => void;
17
+ onViewAttempts: () => void;
18
+ onViewSnapshots: () => void;
19
+ };
20
+
21
+ const { onConfigure, onViewAttempts, onViewSnapshots }: Props = $props();
22
+
23
+ const backup = useImmichBackupStatus();
24
+
25
+ const { repository, schedule } = $derived(backup);
26
+
27
+ const { ViewRecoveryKey, Configure } = $derived(
28
+ getBackupPageActions(repository?.id, onConfigure),
29
+ );
30
+ </script>
31
+
32
+ <OnEvents {...useImmichBackupStatusEventHandler()} />
33
+
34
+ <PageLayout title="Backups" actions={[ViewRecoveryKey, Configure]}>
35
+ <Container size="medium" center>
36
+ {#if repository && schedule}
37
+ <Stack class="mt-4" gap={6}>
38
+ <ImmichManageBackupOverview {repository} {schedule} status={backup.status} />
39
+ <BackendsList {repository} />
40
+ <RepositoryRunHistory {repository} onViewAll={onViewAttempts} />
41
+ <RepositorySnapshotsList
42
+ {repository}
43
+ immich
44
+ limit={5}
45
+ onViewAll={onViewSnapshots}
46
+ />
47
+ </Stack>
48
+ {/if}
49
+ </Container>
50
+ </PageLayout>
@@ -0,0 +1,8 @@
1
+ type Props = {
2
+ onConfigure: () => void;
3
+ onViewAttempts: () => void;
4
+ onViewSnapshots: () => void;
5
+ };
6
+ declare const ImmichManageBackupPage: import("svelte").Component<Props, {}, "">;
7
+ type ImmichManageBackupPage = ReturnType<typeof ImmichManageBackupPage>;
8
+ export default ImmichManageBackupPage;
@@ -0,0 +1,19 @@
1
+ <script lang="ts">
2
+ import RepositorySnapshotsPage from "../../backups/snapshots-list/RepositorySnapshotsPage.svelte";
3
+ import { Container } from "@immich/ui";
4
+ import ImmichConfiguredGate from "./ImmichConfiguredGate.svelte";
5
+
6
+ type Props = {
7
+ onUnconfigured: () => void;
8
+ };
9
+
10
+ const { onUnconfigured }: Props = $props();
11
+ </script>
12
+
13
+ <ImmichConfiguredGate {onUnconfigured}>
14
+ {#snippet children(repository)}
15
+ <Container size="medium" center>
16
+ <RepositorySnapshotsPage {repository} immich />
17
+ </Container>
18
+ {/snippet}
19
+ </ImmichConfiguredGate>
@@ -0,0 +1,6 @@
1
+ type Props = {
2
+ onUnconfigured: () => void;
3
+ };
4
+ declare const ImmichSnapshotsPage: import("svelte").Component<Props, {}, "">;
5
+ type ImmichSnapshotsPage = ReturnType<typeof ImmichSnapshotsPage>;
6
+ export default ImmichSnapshotsPage;
@@ -41,7 +41,11 @@
41
41
  import MockImmichServerStatus from "./immich/MockImmichServerStatus.svelte";
42
42
  import MockImmichPurchaseInfo from "./immich/MockImmichPurchaseInfo.svelte";
43
43
  import MockImmichStorageSpace from "./immich/MockImmichStorageSpace.svelte";
44
+ import PageLayout from "../ui/PageLayout.svelte";
45
+ import ImmichBackupAttemptsPage from "../integrations/immich/ImmichBackupAttemptsPage.svelte";
46
+ import ImmichBackupSettingsPage from "../integrations/immich/ImmichBackupSettingsPage.svelte";
44
47
  import ImmichBackupsPage from "../integrations/immich/ImmichBackupsPage.svelte";
48
+ import ImmichSnapshotsPage from "../integrations/immich/ImmichSnapshotsPage.svelte";
45
49
  import ImmichOnboardingRestoreFlow from "../integrations/immich/ImmichOnboardingRestoreFlow.svelte";
46
50
  import MockImmichHideBackupsReminder from "./immich/MockImmichHideBackupsReminder.svelte";
47
51
 
@@ -52,7 +56,13 @@
52
56
  const { onExit }: Props = $props();
53
57
  const { testUiRestore, demoPadding } = options;
54
58
 
55
- let route = $state<"photos" | "settings">("photos");
59
+ let route = $state<
60
+ | "photos"
61
+ | "settings"
62
+ | "backup-settings"
63
+ | "backup-attempts"
64
+ | "backup-snapshots"
65
+ >("photos");
56
66
 
57
67
  const sampleQuestions = [
58
68
  {
@@ -69,6 +79,8 @@
69
79
  },
70
80
  ];
71
81
 
82
+ const backToBackups = () => (route = "settings");
83
+
72
84
  demoPadding.set(true);
73
85
  onDestroy(() => demoPadding.set(false));
74
86
  </script>
@@ -186,7 +198,7 @@
186
198
 
187
199
  <AppShellSidebar class="relative">
188
200
  <div class="flex h-full flex-col pt-4 pr-2">
189
- {#if route === "settings"}
201
+ {#if route !== "photos"}
190
202
  <ImmichBackupsAdminNavButton href="#" />
191
203
 
192
204
  <NavbarItem
@@ -247,7 +259,7 @@
247
259
  <ImmichBackupsSidebarItem href="#" />
248
260
  </MockImmichStorageSpace>
249
261
 
250
- {#if route !== "settings"}
262
+ {#if route === "photos"}
251
263
  <MockImmichPurchaseInfo />
252
264
  {/if}
253
265
 
@@ -272,7 +284,22 @@
272
284
  },
273
285
  ...sampleQuestions,
274
286
  ]}
287
+ onConfigure={() => (route = "backup-settings")}
288
+ onViewAttempts={() => (route = "backup-attempts")}
289
+ onViewSnapshots={() => (route = "backup-snapshots")}
275
290
  />
291
+ {:else if route === "backup-settings"}
292
+ <PageLayout title="Backup settings" onBack={backToBackups}>
293
+ <ImmichBackupSettingsPage onUnconfigured={backToBackups} />
294
+ </PageLayout>
295
+ {:else if route === "backup-attempts"}
296
+ <PageLayout title="Backup attempts" onBack={backToBackups}>
297
+ <ImmichBackupAttemptsPage onUnconfigured={backToBackups} />
298
+ </PageLayout>
299
+ {:else if route === "backup-snapshots"}
300
+ <PageLayout title="Snapshots" onBack={backToBackups}>
301
+ <ImmichSnapshotsPage onUnconfigured={backToBackups} />
302
+ </PageLayout>
276
303
  {:else}
277
304
  <MockImmichPhotos />
278
305
  {/if}
@@ -15,7 +15,7 @@
15
15
  } from "@mdi/js";
16
16
  import GlobalSettings from "../settings/GlobalSettings.svelte";
17
17
  import BackupsList from "../backups/BackupsList.svelte";
18
- import Dashboard from "../dashboard/Dashboard.svelte";
18
+ import DashboardPage from "../dashboard/DashboardPage.svelte";
19
19
  import ScheduleList from "../schedules/ScheduleList.svelte";
20
20
 
21
21
  let open = $state(true);
@@ -99,7 +99,7 @@
99
99
 
100
100
  <div class="p-4 flex flex-col gap-2 max-w-6xl m-auto">
101
101
  {#if route === "dashboard"}
102
- <Dashboard onViewBackups={() => (route = "backups")} />
102
+ <DashboardPage onViewBackups={() => (route = "backups")} />
103
103
  {:else if route === "backups"}
104
104
  <BackupsList />
105
105
  {:else if route === "config"}
@@ -5,7 +5,7 @@
5
5
  value: number;
6
6
  label: string;
7
7
  color: string;
8
- badge: "success" | "warning" | "danger" | "secondary";
8
+ badge: "success" | "warning" | "danger" | "secondary" | "info";
9
9
  };
10
10
 
11
11
  type Props = {
@@ -2,7 +2,7 @@ type Segment = {
2
2
  value: number;
3
3
  label: string;
4
4
  color: string;
5
- badge: "success" | "warning" | "danger" | "secondary";
5
+ badge: "success" | "warning" | "danger" | "secondary" | "info";
6
6
  };
7
7
  type Props = {
8
8
  title: string;
@@ -81,11 +81,11 @@ export type IntegrationsResponseDto = {
81
81
  immichState?: ImmichStateDto;
82
82
  immichIntegration?: ImmichIntegrationDto;
83
83
  };
84
- export type TaskStatus = "incomplete" | "complete" | "warn" | "failed";
85
84
  export type RepositoryMetricsDto = {
85
+ lastStarted?: string | null;
86
86
  lastBackup?: string | null;
87
- lastBackupStatus?: TaskStatus;
88
- lastBackupDuration?: number;
87
+ lastBackupStatus?: ("incomplete" | "complete" | "warn" | "failed" | "cancelled" | null) | null;
88
+ lastBackupDuration?: number | null;
89
89
  sizeBytes: number;
90
90
  };
91
91
  export type RepositoryMeterDto = {
@@ -135,7 +135,7 @@ export type ScheduleDto = {
135
135
  lastRun?: string;
136
136
  lastFinished?: string;
137
137
  };
138
- export type RunStatus = "incomplete" | "complete" | "warn" | "failed";
138
+ export type RunStatus = "incomplete" | "complete" | "warn" | "failed" | "cancelled";
139
139
  export type RunType = "schedule" | "restore" | "backup" | "forget";
140
140
  export type RunDto = {
141
141
  id: string;
@@ -162,6 +162,7 @@ export type ConfigureImmichIntegrationRequestDto = {
162
162
  libraries: "all" | string[];
163
163
  retentionPolicy?: (RetentionPolicyDto) | null;
164
164
  paused?: boolean;
165
+ repositoryId?: string;
165
166
  };
166
167
  export type ConfigureImmichIntegrationResponseDto = {
167
168
  repositoryId: string;
@@ -197,6 +198,7 @@ export type RepositoryCreateRequestDto = {
197
198
  /** Internal site code from environment metadata */
198
199
  site?: string;
199
200
  paths?: string[];
201
+ retentionPolicy?: (RetentionPolicyDto) | null;
200
202
  };
201
203
  export type RepositoryCreateResponseDto = {
202
204
  repository: LocalRepositoryDto;
@@ -270,6 +272,7 @@ export type RunResponseDto = {
270
272
  run: RunDto;
271
273
  };
272
274
  export type TaskType = "schedule" | "restore" | "backup" | "forget";
275
+ export type TaskStatus = "incomplete" | "complete" | "warn" | "failed" | "cancelled";
273
276
  export type ActiveScheduleItemDto = {
274
277
  repositoryId: string;
275
278
  status: TaskStatus;
@@ -285,6 +288,7 @@ export type RunningTaskListResponse = {
285
288
  };
286
289
  export type ScheduleCreateRequestDto = {
287
290
  name: string;
291
+ paused?: boolean;
288
292
  cron: string;
289
293
  repositories: string[];
290
294
  };
@@ -349,6 +353,7 @@ export declare function getSnapshotListing(id: string, snapshot: string, { path
349
353
  path?: string;
350
354
  }, opts?: Oazapfts.RequestOpts): Promise<FilesystemListingResponseDto>;
351
355
  export declare function getRun(id: string, opts?: Oazapfts.RequestOpts): Promise<RunResponseDto>;
356
+ export declare function downloadRunLog(id: string, opts?: Oazapfts.RequestOpts): Promise<Blob>;
352
357
  export declare function logStreamSse(id: string, opts?: Oazapfts.RequestOpts): Promise<never>;
353
358
  export declare function getRunningTasks(opts?: Oazapfts.RequestOpts): Promise<RunningTaskListResponse>;
354
359
  export declare function cancelTask(parentId: string, opts?: Oazapfts.RequestOpts): Promise<never>;
@@ -240,6 +240,11 @@ export function getRun(id, opts) {
240
240
  ...opts
241
241
  }));
242
242
  }
243
+ export function downloadRunLog(id, opts) {
244
+ return oazapfts.ok(oazapfts.fetchBlob(`/api/yucca/logs/${encodeURIComponent(id)}/download`, {
245
+ ...opts
246
+ }));
247
+ }
243
248
  export function logStreamSse(id, opts) {
244
249
  return oazapfts.ok(oazapfts.fetchText(`/api/yucca/logs/${encodeURIComponent(id)}/stream`, {
245
250
  ...opts
package/dist/index.d.ts CHANGED
@@ -2,13 +2,16 @@ export { default as Suspense } from './components/util/Suspense.svelte';
2
2
  export { default as BackendsList } from './components/backends/BackendsList.svelte';
3
3
  export { default as BackupsList } from './components/backups/BackupsList.svelte';
4
4
  export { default as ViewStatusModal } from './components/backups/dialogs/ViewStatusModal.svelte';
5
- export { default as Dashboard } from './components/dashboard/Dashboard.svelte';
5
+ export { default as DashboardPage } from './components/dashboard/DashboardPage.svelte';
6
+ export { default as ImmichBackupAttemptsPage } from './components/integrations/immich/ImmichBackupAttemptsPage.svelte';
7
+ export { default as ImmichBackupSettingsPage } from './components/integrations/immich/ImmichBackupSettingsPage.svelte';
6
8
  export { default as ImmichBackupsAdminNavButton } from './components/integrations/immich/ImmichBackupsAdminNavButton.svelte';
7
9
  export { default as ImmichBackupsPage } from './components/integrations/immich/ImmichBackupsPage.svelte';
8
10
  export { default as ImmichBackupsSidebarItem } from './components/integrations/immich/ImmichBackupsSidebarItem.svelte';
9
- export { default as ImmichManageBackup } from './components/integrations/immich/ImmichManageBackup.svelte';
11
+ export { default as ImmichManageBackupPage } from './components/integrations/immich/ImmichManageBackupPage.svelte';
10
12
  export { default as ImmichOnboardingRestoreFlow } from './components/integrations/immich/ImmichOnboardingRestoreFlow.svelte';
11
13
  export { default as ImmichOnboardingSetupFlow } from './components/integrations/immich/ImmichOnboardingSetupFlow.svelte';
14
+ export { default as ImmichSnapshotsPage } from './components/integrations/immich/ImmichSnapshotsPage.svelte';
12
15
  export { default as OnboardingGate } from './components/onboarding/OnboardingGate.svelte';
13
16
  export { default as UpsellModal } from './components/onboarding/upsell/UpsellModal.svelte';
14
17
  export { default as UpsellPage } from './components/onboarding/upsell/UpsellPage.svelte';
package/dist/index.js CHANGED
@@ -2,13 +2,16 @@ export { default as Suspense } from './components/util/Suspense.svelte';
2
2
  export { default as BackendsList } from './components/backends/BackendsList.svelte';
3
3
  export { default as BackupsList } from './components/backups/BackupsList.svelte';
4
4
  export { default as ViewStatusModal } from './components/backups/dialogs/ViewStatusModal.svelte';
5
- export { default as Dashboard } from './components/dashboard/Dashboard.svelte';
5
+ export { default as DashboardPage } from './components/dashboard/DashboardPage.svelte';
6
+ export { default as ImmichBackupAttemptsPage } from './components/integrations/immich/ImmichBackupAttemptsPage.svelte';
7
+ export { default as ImmichBackupSettingsPage } from './components/integrations/immich/ImmichBackupSettingsPage.svelte';
6
8
  export { default as ImmichBackupsAdminNavButton } from './components/integrations/immich/ImmichBackupsAdminNavButton.svelte';
7
9
  export { default as ImmichBackupsPage } from './components/integrations/immich/ImmichBackupsPage.svelte';
8
10
  export { default as ImmichBackupsSidebarItem } from './components/integrations/immich/ImmichBackupsSidebarItem.svelte';
9
- export { default as ImmichManageBackup } from './components/integrations/immich/ImmichManageBackup.svelte';
11
+ export { default as ImmichManageBackupPage } from './components/integrations/immich/ImmichManageBackupPage.svelte';
10
12
  export { default as ImmichOnboardingRestoreFlow } from './components/integrations/immich/ImmichOnboardingRestoreFlow.svelte';
11
13
  export { default as ImmichOnboardingSetupFlow } from './components/integrations/immich/ImmichOnboardingSetupFlow.svelte';
14
+ export { default as ImmichSnapshotsPage } from './components/integrations/immich/ImmichSnapshotsPage.svelte';
12
15
  export { default as OnboardingGate } from './components/onboarding/OnboardingGate.svelte';
13
16
  export { default as UpsellModal } from './components/onboarding/upsell/UpsellModal.svelte';
14
17
  export { default as UpsellPage } from './components/onboarding/upsell/UpsellPage.svelte';
@@ -18,7 +18,7 @@ export declare const useImmichBackupStatusEventHandler: () => {
18
18
  export type ImmichBackupStatus = {
19
19
  kind: 'loading' | 'offline' | 'missing' | 'running' | 'paused' | 'unconfigured' | 'never';
20
20
  } | {
21
- kind: 'complete' | 'warn' | 'failed';
21
+ kind: 'complete' | 'warn' | 'incomplete' | 'failed' | 'cancelled';
22
22
  lastBackup: string;
23
23
  };
24
24
  export declare const useImmichBackupStatus: () => {
@@ -23,4 +23,5 @@ export declare const useRunEventHandler: () => {
23
23
  export declare const handleGetRunHistory: (id: string) => Promise<sdk.RunHistoryResponseDto>;
24
24
  export declare const getRunActions: (run: RunDto) => {
25
25
  ViewLog: ActionItem;
26
+ DownloadLog: ActionItem;
26
27
  };
@@ -2,6 +2,7 @@ import { sdk } from '..';
2
2
  import ViewStatusModal from '../components/backups/dialogs/ViewStatusModal.svelte';
3
3
  import { SocketEvent } from '../events';
4
4
  import { getRun, getRunHistory } from '../fetch-client';
5
+ import { getProvider } from '../providers';
5
6
  import { queryClient } from '../query-client';
6
7
  import { handleError } from '../utils/handle-error';
7
8
  import { modalManager } from '@immich/ui';
@@ -51,5 +52,9 @@ export const getRunActions = (run) => {
51
52
  title: 'View Log',
52
53
  onAction: () => void modalManager.open(ViewStatusModal, { logId: run.id }),
53
54
  };
54
- return { ViewLog };
55
+ const DownloadLog = {
56
+ title: 'Download Full Log',
57
+ onAction: () => window.open(`${getProvider().baseUrl}/api/yucca/logs/${run.id}/download`, '_blank'),
58
+ };
59
+ return { ViewLog, DownloadLog };
55
60
  };
@@ -1,3 +1,3 @@
1
1
  import type { RepositoryMetricsDto } from '../fetch-client';
2
- export type BackupOutcome = 'never' | 'complete' | 'warn' | 'failed';
2
+ export type BackupOutcome = 'never' | 'complete' | 'warn' | 'incomplete' | 'failed' | 'cancelled';
3
3
  export declare const getBackupOutcome: (metrics: RepositoryMetricsDto | undefined) => BackupOutcome;
@@ -1,4 +1,7 @@
1
1
  export const getBackupOutcome = (metrics) => {
2
+ if (metrics?.lastBackupStatus === 'incomplete') {
3
+ return 'incomplete';
4
+ }
2
5
  if (!metrics?.lastBackup) {
3
6
  return 'never';
4
7
  }
@@ -6,6 +9,9 @@ export const getBackupOutcome = (metrics) => {
6
9
  case 'failed': {
7
10
  return 'failed';
8
11
  }
12
+ case 'cancelled': {
13
+ return 'cancelled';
14
+ }
9
15
  case 'warn': {
10
16
  return 'warn';
11
17
  }
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@futo-org/backups-orchestrator-ui",
3
3
  "repository": "https://github.com/immich-app/yucca",
4
4
  "description": "Backups orchestrator (UI library)",
5
- "version": "0.43.0",
5
+ "version": "0.45.0",
6
6
  "license": "Source First License 1.1",
7
7
  "files": [
8
8
  "dist",
@@ -55,7 +55,7 @@
55
55
  "svelte"
56
56
  ],
57
57
  "dependencies": {
58
- "@immich/ui": "^0.85.0",
58
+ "@immich/ui": "^0.89.0",
59
59
  "@mdi/js": "^7.4.47",
60
60
  "@oazapfts/runtime": "^1.1.0",
61
61
  "@tanstack/svelte-query": "^6.1.3",
@@ -64,7 +64,7 @@
64
64
  "lodash.debounce": "^4.0.8",
65
65
  "luxon": "^3.7.2",
66
66
  "socket.io-client": "^4.8.3",
67
- "@futo-org/backups-api-client": "^0.43.0"
67
+ "@futo-org/backups-api-client": "^0.45.0"
68
68
  },
69
69
  "scripts": {
70
70
  "dev": "vite dev --port 5174",
@@ -1,12 +0,0 @@
1
- <script lang="ts">
2
- import { Container } from "@immich/ui";
3
- import ImmichSettingsBackupContents from "./settings/ImmichSettingsBackupContents.svelte";
4
- import ImmichSettingsSchedule from "./settings/ImmichSettingsSchedule.svelte";
5
- import ImmichSettingsStorage from "./settings/ImmichSettingsStorage.svelte";
6
- </script>
7
-
8
- <Container size="large" center>
9
- <ImmichSettingsBackupContents />
10
- <ImmichSettingsSchedule />
11
- <ImmichSettingsStorage />
12
- </Container>
@@ -1,18 +0,0 @@
1
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
- $$bindings?: Bindings;
4
- } & Exports;
5
- (internal: unknown, props: {
6
- $$events?: Events;
7
- $$slots?: Slots;
8
- }): Exports & {
9
- $set?: any;
10
- $on?: any;
11
- };
12
- z_$$bindings?: Bindings;
13
- }
14
- declare const ImmichBackupSettings: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
- [evt: string]: CustomEvent<any>;
16
- }, {}, {}, string>;
17
- type ImmichBackupSettings = InstanceType<typeof ImmichBackupSettings>;
18
- export default ImmichBackupSettings;
@@ -1,76 +0,0 @@
1
- <script lang="ts">
2
- import BackendsList from "../../backends/BackendsList.svelte";
3
- import RepositoryRunHistory from "../../backups/run-history/RepositoryRunHistory.svelte";
4
- import RepositorySnapshotsList from "../../backups/snapshots-list/RepositorySnapshotsList.svelte";
5
- import PageLayout from "../../ui/PageLayout.svelte";
6
- import OnEvents from "../../util/OnEvents.svelte";
7
- import {
8
- getBackupPageActions,
9
- useImmichBackupStatus,
10
- useImmichBackupStatusEventHandler,
11
- } from "../../../services/immich.integration.service";
12
- import RepositoryRunHistoryPage from "../../backups/run-history/RepositoryRunHistoryPage.svelte";
13
- import RepositorySnapshotsPage from "../../backups/snapshots-list/RepositorySnapshotsPage.svelte";
14
- import ImmichBackupSettings from "./ImmichBackupSettings.svelte";
15
- import { Container, Stack } from "@immich/ui";
16
- import ImmichManageBackupOverview from "./ImmichManageBackupOverview.svelte";
17
-
18
- const backup = useImmichBackupStatus();
19
-
20
- const { repository, schedule } = $derived(backup);
21
-
22
- let view = $state<"overview" | "attempts" | "snapshots" | "settings">(
23
- "overview",
24
- );
25
-
26
- const { ViewRecoveryKey, Configure } = $derived(
27
- getBackupPageActions(repository?.id, () => (view = "settings")),
28
- );
29
- </script>
30
-
31
- <OnEvents {...useImmichBackupStatusEventHandler()} />
32
-
33
- {#if view === "attempts" && repository}
34
- <PageLayout title="Backup attempts" onBack={() => (view = "overview")}>
35
- <Container size="medium" center>
36
- <div class="mt-4">
37
- <RepositoryRunHistoryPage {repository} />
38
- </div>
39
- </Container>
40
- </PageLayout>
41
- {:else if view === "settings"}
42
- <PageLayout title="Backup settings" onBack={() => (view = "overview")}>
43
- <div class="mt-4">
44
- <ImmichBackupSettings />
45
- </div>
46
- </PageLayout>
47
- {:else if view === "snapshots" && repository}
48
- <PageLayout title="Snapshots" onBack={() => (view = "overview")}>
49
- <Container size="medium" center>
50
- <div class="mt-4">
51
- <RepositorySnapshotsPage {repository} immich />
52
- </div>
53
- </Container>
54
- </PageLayout>
55
- {:else}
56
- <PageLayout title="Backups" actions={[ViewRecoveryKey, Configure]}>
57
- <Container size="medium" center>
58
- {#if repository && schedule}
59
- <Stack class="mt-4" gap={6}>
60
- <ImmichManageBackupOverview {repository} {schedule} status={backup.status} />
61
- <BackendsList {repository} />
62
- <RepositoryRunHistory
63
- {repository}
64
- onViewAll={() => (view = "attempts")}
65
- />
66
- <RepositorySnapshotsList
67
- {repository}
68
- immich
69
- limit={5}
70
- onViewAll={() => (view = "snapshots")}
71
- />
72
- </Stack>
73
- {/if}
74
- </Container>
75
- </PageLayout>
76
- {/if}
@@ -1,3 +0,0 @@
1
- declare const ImmichManageBackup: import("svelte").Component<Record<string, never>, {}, "">;
2
- type ImmichManageBackup = ReturnType<typeof ImmichManageBackup>;
3
- export default ImmichManageBackup;