@futo-org/backups-orchestrator-ui 0.45.0 → 0.46.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.
@@ -2,6 +2,7 @@
2
2
  import { Container } from "@immich/ui";
3
3
  import ImmichConfiguredGate from "./ImmichConfiguredGate.svelte";
4
4
  import ImmichSettingsBackupContents from "./settings/ImmichSettingsBackupContents.svelte";
5
+ import ImmichSettingsBandwidth from "./settings/ImmichSettingsBandwidth.svelte";
5
6
  import ImmichSettingsSchedule from "./settings/ImmichSettingsSchedule.svelte";
6
7
  import ImmichSettingsStorage from "./settings/ImmichSettingsStorage.svelte";
7
8
 
@@ -17,6 +18,7 @@
17
18
  <Container size="large" center>
18
19
  <ImmichSettingsBackupContents />
19
20
  <ImmichSettingsSchedule />
21
+ <ImmichSettingsBandwidth />
20
22
  <ImmichSettingsStorage />
21
23
  </Container>
22
24
  {/snippet}
@@ -7,6 +7,7 @@
7
7
  } from "../../../services/immich.integration.service";
8
8
  import { HStack, Icon, LoadingSpinner, Text } from "@immich/ui";
9
9
  import {
10
+ mdiAlertCircleOutline,
10
11
  mdiChevronRight,
11
12
  mdiCloudAlertOutline,
12
13
  mdiCloudCheckVariantOutline,
@@ -29,6 +30,10 @@
29
30
  const configured = $derived(status.kind !== "unconfigured");
30
31
 
31
32
  const appearance = $derived.by(() => {
33
+ if (backup.needsAttention) {
34
+ return { color: "warning", icon: mdiAlertCircleOutline } as const;
35
+ }
36
+
32
37
  switch (status.kind) {
33
38
  case "loading": {
34
39
  return { color: "secondary", icon: undefined } as const;
@@ -85,7 +90,9 @@
85
90
  {/if}
86
91
 
87
92
  <Text size="tiny" class="flex-1 leading-tight">
88
- {#if status.kind === "loading"}
93
+ {#if backup.needsAttention}
94
+ Needs attention
95
+ {:else if status.kind === "loading"}
89
96
  Checking backup status
90
97
  {:else if status.kind === "offline"}
91
98
  Backup service is offline
@@ -0,0 +1,42 @@
1
+ <script lang="ts">
2
+ import {
3
+ useConfigureImmichDatabaseDump,
4
+ useIgnoreImmichDatabaseDumpWarning,
5
+ useImmichBackupStatus,
6
+ } from "../../../services/immich.integration.service";
7
+ import { Alert, Button, HStack, Stack, Text } from "@immich/ui";
8
+
9
+ const backup = useImmichBackupStatus();
10
+ const configure = useConfigureImmichDatabaseDump();
11
+ const ignore = useIgnoreImmichDatabaseDumpWarning();
12
+ </script>
13
+
14
+ {#if backup.needsAttention}
15
+ <Alert color="warning" title="Immich database backups are still enabled">
16
+ <Stack gap={3}>
17
+ <Text size="small">
18
+ FUTO Backups already creates the same database backups as Immich.
19
+ <br />
20
+ Most users should select "use FUTO Backups only".
21
+ </Text>
22
+ <HStack gap={2}>
23
+ <Button
24
+ size="small"
25
+ color="primary"
26
+ loading={configure.isPending}
27
+ onclick={() => configure.mutate({ enabled: false })}
28
+ >
29
+ Use FUTO Backups only
30
+ </Button>
31
+ <Button
32
+ size="small"
33
+ variant="ghost"
34
+ loading={ignore.isPending}
35
+ onclick={() => ignore.mutate()}
36
+ >
37
+ Ignore
38
+ </Button>
39
+ </HStack>
40
+ </Stack>
41
+ </Alert>
42
+ {/if}
@@ -0,0 +1,18 @@
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 ImmichDatabaseDumpAlert: $$__sveltets_2_IsomorphicComponent<Record<string, never>, {
15
+ [evt: string]: CustomEvent<any>;
16
+ }, {}, {}, string>;
17
+ type ImmichDatabaseDumpAlert = InstanceType<typeof ImmichDatabaseDumpAlert>;
18
+ export default ImmichDatabaseDumpAlert;
@@ -10,6 +10,7 @@
10
10
  useImmichBackupStatusEventHandler,
11
11
  } from "../../../services/immich.integration.service";
12
12
  import { Container, Stack } from "@immich/ui";
13
+ import ImmichDatabaseDumpAlert from "./ImmichDatabaseDumpAlert.svelte";
13
14
  import ImmichManageBackupOverview from "./ImmichManageBackupOverview.svelte";
14
15
 
15
16
  type Props = {
@@ -35,6 +36,7 @@
35
36
  <Container size="medium" center>
36
37
  {#if repository && schedule}
37
38
  <Stack class="mt-4" gap={6}>
39
+ <ImmichDatabaseDumpAlert />
38
40
  <ImmichManageBackupOverview {repository} {schedule} status={backup.status} />
39
41
  <BackendsList {repository} />
40
42
  <RepositoryRunHistory {repository} onViewAll={onViewAttempts} />
@@ -0,0 +1,150 @@
1
+ <script lang="ts">
2
+ import Accordion from "../../../ui/Accordion.svelte";
3
+ import { useConfig, useUpdateConfig } from "../../../../services/config.service";
4
+ import {
5
+ Button,
6
+ Field,
7
+ HStack,
8
+ LoadingSpinner,
9
+ Select,
10
+ Stack,
11
+ Switch,
12
+ Text,
13
+ } from "@immich/ui";
14
+ import { mdiSpeedometerSlow } from "@mdi/js";
15
+
16
+ const bytesPerMegabyte = 1_000_000;
17
+ const unlimited = "0";
18
+
19
+ const speeds = [
20
+ { value: unlimited, label: "No limit" },
21
+ ...[0.5, 1, 2, 5, 10, 20, 50, 100].map((megabytes) => ({
22
+ value: String(megabytes * bytesPerMegabyte),
23
+ label: `${megabytes} MB/s`,
24
+ })),
25
+ ];
26
+
27
+ const hours = Array.from({ length: 24 }, (_, hour) => ({
28
+ value: `${String(hour).padStart(2, "0")}:00`,
29
+ label: `${String(hour).padStart(2, "0")}:00`,
30
+ }));
31
+
32
+ const defaultQuietHoursStart = "22:00";
33
+ const defaultQuietHoursEnd = "06:00";
34
+
35
+ const config = useConfig();
36
+ const mutation = useUpdateConfig();
37
+
38
+ let speed = $state(unlimited);
39
+ let quiet = $state(false);
40
+ let quietStart = $state(defaultQuietHoursStart);
41
+ let quietEnd = $state(defaultQuietHoursEnd);
42
+ let loaded = false;
43
+
44
+ const load = () => {
45
+ if (!config.data) {
46
+ return;
47
+ }
48
+
49
+ const { bytesPerSec, quietHours } = config.data.bandwidth;
50
+ const [start = defaultQuietHoursStart, end = defaultQuietHoursEnd] =
51
+ quietHours?.split("-") ?? [];
52
+
53
+ speed = String(bytesPerSec);
54
+ quiet = Boolean(quietHours);
55
+ quietStart = start;
56
+ quietEnd = end;
57
+ };
58
+
59
+ $effect(() => {
60
+ if (loaded || !config.data) {
61
+ return;
62
+ }
63
+
64
+ loaded = true;
65
+ load();
66
+ });
67
+
68
+ const limited = $derived(speed !== unlimited);
69
+
70
+ const onSave = () => {
71
+ mutation.mutate({
72
+ bandwidth: {
73
+ bytesPerSec: Number(speed),
74
+ quietHours: limited && quiet ? `${quietStart}-${quietEnd}` : undefined,
75
+ },
76
+ });
77
+ };
78
+ </script>
79
+
80
+ <Accordion
81
+ title="Bandwidth"
82
+ subtitle="Manage how fast backups upload."
83
+ icon={mdiSpeedometerSlow}
84
+ >
85
+ <Stack gap={4} class="pt-2">
86
+ {#if config.isLoading}
87
+ <LoadingSpinner />
88
+ {:else}
89
+ <Field
90
+ label="Upload speed"
91
+ description="Slow down backup uploads without affecting restores."
92
+ color="primary"
93
+ >
94
+ <Select
95
+ options={speeds}
96
+ value={speed}
97
+ onChange={(value) => (speed = value)}
98
+ />
99
+ </Field>
100
+
101
+ <Field
102
+ label="Full speed during quiet hours"
103
+ description="Lift the limit while you are asleep."
104
+ color="primary"
105
+ disabled={!limited}
106
+ >
107
+ <Switch bind:checked={quiet} />
108
+ </Field>
109
+
110
+ {#if limited && quiet}
111
+ <HStack gap={4}>
112
+ <Field label="From" color="primary">
113
+ <Select
114
+ options={hours}
115
+ value={quietStart}
116
+ onChange={(value) => (quietStart = value)}
117
+ />
118
+ </Field>
119
+ <Field label="Until" color="primary">
120
+ <Select
121
+ options={hours}
122
+ value={quietEnd}
123
+ onChange={(value) => (quietEnd = value)}
124
+ />
125
+ </Field>
126
+ </HStack>
127
+
128
+ {#if quietStart === quietEnd}
129
+ <Text size="small" color="muted">
130
+ With the same start and end time, the limit never applies.
131
+ </Text>
132
+ {/if}
133
+ {/if}
134
+
135
+ <HStack gap={2} class="justify-end">
136
+ <Button
137
+ shape="round"
138
+ color="secondary"
139
+ disabled={mutation.isPending}
140
+ onclick={load}
141
+ >
142
+ Reset
143
+ </Button>
144
+ <Button shape="round" loading={mutation.isPending} onclick={onSave}>
145
+ Save
146
+ </Button>
147
+ </HStack>
148
+ {/if}
149
+ </Stack>
150
+ </Accordion>
@@ -0,0 +1,3 @@
1
+ declare const ImmichSettingsBandwidth: import("svelte").Component<Record<string, never>, {}, "">;
2
+ type ImmichSettingsBandwidth = ReturnType<typeof ImmichSettingsBandwidth>;
3
+ export default ImmichSettingsBandwidth;
@@ -47,6 +47,16 @@ export type CreateLocalBackendRequestDto = {
47
47
  export type BackendResponseDto = {
48
48
  backend: BackendDto;
49
49
  };
50
+ export type BandwidthDto = {
51
+ bytesPerSec: number;
52
+ quietHours?: string;
53
+ };
54
+ export type ConfigResponseDto = {
55
+ bandwidth: BandwidthDto;
56
+ };
57
+ export type ConfigUpdateRequestDto = {
58
+ bandwidth: BandwidthDto;
59
+ };
50
60
  export type FilesystemListingItemDto = {
51
61
  path: string;
52
62
  isDirectory: boolean;
@@ -146,12 +156,18 @@ export type RunDto = {
146
156
  status: RunStatus;
147
157
  "type": RunType;
148
158
  };
159
+ export type ImmichDatabaseDumpConfigDto = {
160
+ enabled: boolean;
161
+ keepLastAmount: number;
162
+ };
149
163
  export type ImmichBackupStatusDto = {
150
164
  integration?: ImmichIntegrationDto;
151
165
  repository?: LocalRepositoryDto;
152
166
  backend?: BackendDto;
153
167
  schedule?: ScheduleDto;
154
168
  latestBackupRun?: RunDto;
169
+ databaseDump?: ImmichDatabaseDumpConfigDto;
170
+ databaseDumpWarningIgnored?: boolean;
155
171
  };
156
172
  export type ConfigureImmichIntegrationRequestDto = {
157
173
  name: string;
@@ -167,6 +183,10 @@ export type ConfigureImmichIntegrationRequestDto = {
167
183
  export type ConfigureImmichIntegrationResponseDto = {
168
184
  repositoryId: string;
169
185
  };
186
+ export type ConfigureImmichDatabaseDumpRequestDto = {
187
+ enabled?: boolean;
188
+ keepLastAmount?: number;
189
+ };
170
190
  export type ImmichRollbackRequestDto = {
171
191
  repositoryId: string;
172
192
  snapshotId: string;
@@ -313,6 +333,8 @@ export declare function createSession(createSessionRequestDto: CreateSessionRequ
313
333
  export declare function createTicket(ticketCreateRequestDto: TicketCreateRequestDto, opts?: Oazapfts.RequestOpts): Promise<TicketCreateResponseDto>;
314
334
  export declare function getBackends(opts?: Oazapfts.RequestOpts): Promise<BackendsResponseDto>;
315
335
  export declare function createLocalBackend(createLocalBackendRequestDto: CreateLocalBackendRequestDto, opts?: Oazapfts.RequestOpts): Promise<BackendResponseDto>;
336
+ export declare function getConfig(opts?: Oazapfts.RequestOpts): Promise<ConfigResponseDto>;
337
+ export declare function updateConfig(configUpdateRequestDto: ConfigUpdateRequestDto, opts?: Oazapfts.RequestOpts): Promise<ConfigResponseDto>;
316
338
  export declare function resetOrchestrator(opts?: Oazapfts.RequestOpts): Promise<never>;
317
339
  export declare function getFileListing({ path }?: {
318
340
  path?: string;
@@ -320,6 +342,8 @@ export declare function getFileListing({ path }?: {
320
342
  export declare function getIntegrations(opts?: Oazapfts.RequestOpts): Promise<IntegrationsResponseDto>;
321
343
  export declare function getImmichBackupStatus(opts?: Oazapfts.RequestOpts): Promise<ImmichBackupStatusDto>;
322
344
  export declare function configureImmichIntegration(configureImmichIntegrationRequestDto: ConfigureImmichIntegrationRequestDto, opts?: Oazapfts.RequestOpts): Promise<ConfigureImmichIntegrationResponseDto>;
345
+ export declare function ignoreImmichDatabaseDumpWarning(opts?: Oazapfts.RequestOpts): Promise<never>;
346
+ export declare function configureImmichDatabaseDump(configureImmichDatabaseDumpRequestDto: ConfigureImmichDatabaseDumpRequestDto, opts?: Oazapfts.RequestOpts): Promise<never>;
323
347
  export declare function startImmichRollback(immichRollbackRequestDto: ImmichRollbackRequestDto, opts?: Oazapfts.RequestOpts): Promise<never>;
324
348
  export declare function onboardingStatus(opts?: Oazapfts.RequestOpts): Promise<OnboardingStatusResponseDto>;
325
349
  export declare function currentRecoveryKey(opts?: Oazapfts.RequestOpts): Promise<CurrentRecoveryKeyResponse>;
@@ -48,6 +48,18 @@ export function createLocalBackend(createLocalBackendRequestDto, opts) {
48
48
  body: createLocalBackendRequestDto
49
49
  })));
50
50
  }
51
+ export function getConfig(opts) {
52
+ return oazapfts.ok(oazapfts.fetchJson("/api/yucca/config", {
53
+ ...opts
54
+ }));
55
+ }
56
+ export function updateConfig(configUpdateRequestDto, opts) {
57
+ return oazapfts.ok(oazapfts.fetchJson("/api/yucca/config", oazapfts.json({
58
+ ...opts,
59
+ method: "PUT",
60
+ body: configUpdateRequestDto
61
+ })));
62
+ }
51
63
  export function resetOrchestrator(opts) {
52
64
  return oazapfts.ok(oazapfts.fetchText("/api/yucca/debug/reset", {
53
65
  ...opts,
@@ -78,6 +90,19 @@ export function configureImmichIntegration(configureImmichIntegrationRequestDto,
78
90
  body: configureImmichIntegrationRequestDto
79
91
  })));
80
92
  }
93
+ export function ignoreImmichDatabaseDumpWarning(opts) {
94
+ return oazapfts.ok(oazapfts.fetchText("/api/yucca/integrations/immich/database-dump/ignore-warning", {
95
+ ...opts,
96
+ method: "POST"
97
+ }));
98
+ }
99
+ export function configureImmichDatabaseDump(configureImmichDatabaseDumpRequestDto, opts) {
100
+ return oazapfts.ok(oazapfts.fetchText("/api/yucca/integrations/immich/database-dump", oazapfts.json({
101
+ ...opts,
102
+ method: "POST",
103
+ body: configureImmichDatabaseDumpRequestDto
104
+ })));
105
+ }
81
106
  export function startImmichRollback(immichRollbackRequestDto, opts) {
82
107
  return oazapfts.ok(oazapfts.fetchText("/api/yucca/integrations/immich/rollback", oazapfts.json({
83
108
  ...opts,
@@ -0,0 +1,6 @@
1
+ import { type ConfigUpdateRequestDto } from '../fetch-client';
2
+ export declare const configKeys: {
3
+ all: readonly ["config"];
4
+ };
5
+ export declare const useConfig: () => import("@tanstack/svelte-query").CreateQueryResult<import("../fetch-client").ConfigResponseDto, Error>;
6
+ export declare const useUpdateConfig: () => import("@tanstack/svelte-query").CreateMutationResult<import("../fetch-client").ConfigResponseDto, Error, ConfigUpdateRequestDto, unknown>;
@@ -0,0 +1,16 @@
1
+ import { getConfig, updateConfig, } from '../fetch-client';
2
+ import { queryClient } from '../query-client';
3
+ import { handleError } from '../utils/handle-error';
4
+ import { createMutation, createQuery } from '@tanstack/svelte-query';
5
+ export const configKeys = {
6
+ all: ['config'],
7
+ };
8
+ export const useConfig = () => createQuery(() => ({
9
+ queryKey: configKeys.all,
10
+ queryFn: () => getConfig(),
11
+ }), () => queryClient);
12
+ export const useUpdateConfig = () => createMutation(() => ({
13
+ mutationFn: (dto) => updateConfig(dto),
14
+ onSuccess: (config) => queryClient.setQueryData(configKeys.all, config),
15
+ onError: (error) => handleError(error, 'Failed to save settings'),
16
+ }), () => queryClient);
@@ -1,4 +1,5 @@
1
1
  import { type ActionItem } from '@immich/ui';
2
+ import { type ConfigureImmichDatabaseDumpRequestDto } from '../fetch-client';
2
3
  export declare const getBackupPageActions: (repositoryId: string | undefined, onConfigure: () => void) => {
3
4
  Configure: ActionItem;
4
5
  ViewRecoveryKey: ActionItem;
@@ -25,4 +26,7 @@ export declare const useImmichBackupStatus: () => {
25
26
  readonly repository: import("../fetch-client").LocalRepositoryDto | undefined;
26
27
  readonly schedule: import("../fetch-client").ScheduleDto | undefined;
27
28
  readonly status: ImmichBackupStatus;
29
+ readonly needsAttention: boolean;
28
30
  };
31
+ export declare const useConfigureImmichDatabaseDump: () => import("@tanstack/svelte-query").CreateMutationResult<never, Error, ConfigureImmichDatabaseDumpRequestDto, unknown>;
32
+ export declare const useIgnoreImmichDatabaseDumpWarning: () => import("@tanstack/svelte-query").CreateMutationResult<never, Error, void, unknown>;
@@ -1,10 +1,11 @@
1
1
  import BackupsRecoveryKeyModal from '../components/onboarding/dialogs/BackupsRecoveryKeyModal.svelte';
2
2
  import { modalManager } from '@immich/ui';
3
3
  import { mdiCloudUploadOutline, mdiCogOutline, mdiKeyOutline } from '@mdi/js';
4
- import { getImmichBackupStatus, } from '../fetch-client';
4
+ import { configureImmichDatabaseDump, getImmichBackupStatus, ignoreImmichDatabaseDumpWarning, } from '../fetch-client';
5
5
  import { queryClient } from '../query-client';
6
+ import { handleError } from '../utils/handle-error';
6
7
  import { getBackupOutcome } from '../utils/backup-status';
7
- import { createQuery } from '@tanstack/svelte-query';
8
+ import { createMutation, createQuery } from '@tanstack/svelte-query';
8
9
  import { handleCreateBackup } from './repository.service';
9
10
  export const getBackupPageActions = (repositoryId, onConfigure) => {
10
11
  const Configure = {
@@ -31,6 +32,7 @@ export const immichBackupStatusKeys = {
31
32
  const invalidateImmichBackupStatus = () => void queryClient.invalidateQueries({
32
33
  queryKey: immichBackupStatusKeys.all,
33
34
  });
35
+ const updateImmichBackupStatus = (update) => queryClient.setQueryData(immichBackupStatusKeys.all, (data) => data && update(data));
34
36
  export const useImmichBackupStatusEventHandler = () => ({
35
37
  onIntegrationUpdate: invalidateImmichBackupStatus,
36
38
  onRepositoryUpdate: invalidateImmichBackupStatus,
@@ -77,5 +79,28 @@ export const useImmichBackupStatus = () => {
77
79
  get status() {
78
80
  return toImmichBackupStatus(query.data, query.isLoading);
79
81
  },
82
+ get needsAttention() {
83
+ const usingImmichDbDump = query.data?.databaseDump?.enabled === true &&
84
+ query.data.schedule !== undefined &&
85
+ !query.data.schedule.paused &&
86
+ query.data.databaseDumpWarningIgnored !== true;
87
+ return usingImmichDbDump;
88
+ },
80
89
  };
81
90
  };
91
+ export const useConfigureImmichDatabaseDump = () => createMutation(() => ({
92
+ mutationFn: (dto) => configureImmichDatabaseDump(dto),
93
+ onSuccess: (_, dto) => updateImmichBackupStatus((data) => ({
94
+ ...data,
95
+ databaseDump: data.databaseDump && { ...data.databaseDump, ...dto },
96
+ })),
97
+ onError: (error) => handleError(error, 'Failed to update Immich database dump settings'),
98
+ }), () => queryClient);
99
+ export const useIgnoreImmichDatabaseDumpWarning = () => createMutation(() => ({
100
+ mutationFn: () => ignoreImmichDatabaseDumpWarning(),
101
+ onSuccess: () => updateImmichBackupStatus((data) => ({
102
+ ...data,
103
+ databaseDumpWarningIgnored: true,
104
+ })),
105
+ onError: (error) => handleError(error, 'Failed to ignore Immich database dump warning'),
106
+ }), () => queryClient);
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.45.0",
5
+ "version": "0.46.0",
6
6
  "license": "Source First License 1.1",
7
7
  "files": [
8
8
  "dist",
@@ -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.45.0"
67
+ "@futo-org/backups-api-client": "^0.46.0"
68
68
  },
69
69
  "scripts": {
70
70
  "dev": "vite dev --port 5174",