feat: show progress indicator on mirrors

refactor: remove unnecessary useEffects
This commit is contained in:
Nicolas Meienberger 2026-02-11 21:44:57 +01:00
parent 7ebce1166b
commit d5284d9450
13 changed files with 153 additions and 129 deletions

View file

@ -3311,7 +3311,7 @@ export type GetScheduleMirrorsResponses = {
enabled: boolean; enabled: boolean;
lastCopyAt: number | null; lastCopyAt: number | null;
lastCopyError: string | null; lastCopyError: string | null;
lastCopyStatus: "error" | "success" | null; lastCopyStatus: "error" | "in_progress" | "success" | null;
repository: { repository: {
compressionMode: "auto" | "max" | "off" | null; compressionMode: "auto" | "max" | "off" | null;
config: config:
@ -3531,7 +3531,7 @@ export type UpdateScheduleMirrorsResponses = {
enabled: boolean; enabled: boolean;
lastCopyAt: number | null; lastCopyAt: number | null;
lastCopyError: string | null; lastCopyError: string | null;
lastCopyStatus: "error" | "success" | null; lastCopyStatus: "error" | "in_progress" | "success" | null;
repository: { repository: {
compressionMode: "auto" | "max" | "off" | null; compressionMode: "auto" | "max" | "off" | null;
config: config:

View file

@ -29,7 +29,7 @@ export interface MirrorEvent {
scheduleId: number; scheduleId: number;
repositoryId: string; repositoryId: string;
repositoryName: string; repositoryName: string;
status?: "success" | "error"; status?: "success" | "error" | "in_progress";
error?: string; error?: string;
} }

View file

@ -18,6 +18,7 @@ import { parseError } from "~/client/lib/errors";
import type { Repository } from "~/client/lib/types"; import type { Repository } from "~/client/lib/types";
import { RepositoryIcon } from "~/client/components/repository-icon"; import { RepositoryIcon } from "~/client/components/repository-icon";
import { StatusDot } from "~/client/components/status-dot"; import { StatusDot } from "~/client/components/status-dot";
import { useServerEvents } from "~/client/hooks/use-server-events";
import { formatDistanceToNow } from "date-fns"; import { formatDistanceToNow } from "date-fns";
import { cn } from "~/client/lib/utils"; import { cn } from "~/client/lib/utils";
import type { GetScheduleMirrorsResponse } from "~/client/api-client"; import type { GetScheduleMirrorsResponse } from "~/client/api-client";
@ -34,28 +35,31 @@ type MirrorAssignment = {
repositoryId: string; repositoryId: string;
enabled: boolean; enabled: boolean;
lastCopyAt: number | null; lastCopyAt: number | null;
lastCopyStatus: "success" | "error" | null; lastCopyStatus: "success" | "error" | "in_progress" | null;
lastCopyError: string | null; lastCopyError: string | null;
}; };
const buildAssignments = (mirrors: GetScheduleMirrorsResponse) => { const isSyncing = (assignment: MirrorAssignment) => assignment.lastCopyStatus === "in_progress";
const map = new Map<string, MirrorAssignment>();
for (const mirror of mirrors) { const buildAssignments = (mirrors: GetScheduleMirrorsResponse) =>
map.set(mirror.repositoryId, { new Map<string, MirrorAssignment>(
repositoryId: mirror.repositoryId, mirrors.map((mirror) => [
enabled: mirror.enabled, mirror.repositoryId,
lastCopyAt: mirror.lastCopyAt, {
lastCopyStatus: mirror.lastCopyStatus, repositoryId: mirror.repositoryId,
lastCopyError: mirror.lastCopyError, enabled: mirror.enabled,
}); lastCopyAt: mirror.lastCopyAt,
} lastCopyStatus: mirror.lastCopyStatus,
return map; lastCopyError: mirror.lastCopyError,
}; },
]),
);
export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, repositories, initialData }: Props) => { export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, repositories, initialData }: Props) => {
const [assignments, setAssignments] = useState<Map<string, MirrorAssignment>>(() => buildAssignments(initialData)); const [assignments, setAssignments] = useState<Map<string, MirrorAssignment>>(() => buildAssignments(initialData));
const [hasChanges, setHasChanges] = useState(false); const [hasChanges, setHasChanges] = useState(false);
const [isAddingNew, setIsAddingNew] = useState(false); const [isAddingNew, setIsAddingNew] = useState(false);
const { addEventListener } = useServerEvents();
const { data: currentMirrors } = useSuspenseQuery({ const { data: currentMirrors } = useSuspenseQuery({
...getScheduleMirrorsOptions({ path: { scheduleId: scheduleId.toString() } }), ...getScheduleMirrorsOptions({ path: { scheduleId: scheduleId.toString() } }),
@ -94,6 +98,42 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit
return map; return map;
}, [compatibility]); }, [compatibility]);
useEffect(() => {
const unsubscribeStarted = addEventListener("mirror:started", (data) => {
const event = data as { scheduleId: number; repositoryId: string };
if (event.scheduleId !== scheduleId) return;
setAssignments((prev) => {
const next = new Map(prev);
const existing = next.get(event.repositoryId);
if (!existing) return prev;
next.set(event.repositoryId, { ...existing, lastCopyStatus: "in_progress", lastCopyError: null });
return next;
});
});
const unsubscribeCompleted = addEventListener("mirror:completed", (data) => {
const event = data as { scheduleId: number; repositoryId: string; status?: "success" | "error"; error?: string };
if (event.scheduleId !== scheduleId) return;
setAssignments((prev) => {
const next = new Map(prev);
const existing = next.get(event.repositoryId);
if (!existing) return prev;
next.set(event.repositoryId, {
...existing,
lastCopyStatus: event.status ?? existing.lastCopyStatus,
lastCopyError: event.error ?? null,
lastCopyAt: Date.now(),
});
return next;
});
});
return () => {
unsubscribeStarted();
unsubscribeCompleted();
};
}, [addEventListener, scheduleId]);
const addRepository = (repositoryId: string) => { const addRepository = (repositoryId: string) => {
const newAssignments = new Map(assignments); const newAssignments = new Map(assignments);
newAssignments.set(repositoryId, { newAssignments.set(repositoryId, {
@ -144,20 +184,8 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit
}; };
const handleReset = () => { const handleReset = () => {
if (currentMirrors) { setAssignments(buildAssignments(currentMirrors));
const map = new Map<string, MirrorAssignment>(); setHasChanges(false);
for (const mirror of currentMirrors) {
map.set(mirror.repositoryId, {
repositoryId: mirror.repositoryId,
enabled: mirror.enabled,
lastCopyAt: mirror.lastCopyAt,
lastCopyStatus: mirror.lastCopyStatus,
lastCopyError: mirror.lastCopyError,
});
}
setAssignments(map);
setHasChanges(false);
}
}; };
const selectableRepositories = const selectableRepositories =
@ -176,13 +204,27 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit
.map((id) => repositories?.find((r) => r.id === id)) .map((id) => repositories?.find((r) => r.id === id))
.filter((r) => r !== undefined); .filter((r) => r !== undefined);
const getStatusVariant = (status: "success" | "error" | null) => { const getStatusVariant = (status: string | null) => {
if (status === "success") return "success"; if (status === "success") return "success";
if (status === "error") return "error"; if (status === "error") return "error";
if (status === "in_progress") return "info";
return "neutral"; return "neutral";
}; };
const getLabel = (assignment: MirrorAssignment) => {
if (isSyncing(assignment)) {
return "Syncing...";
}
if (assignment.lastCopyAt) {
return formatDistanceToNow(new Date(assignment.lastCopyAt), { addSuffix: true });
}
return "Never";
};
const getStatusLabel = (assignment: MirrorAssignment) => { const getStatusLabel = (assignment: MirrorAssignment) => {
if (isSyncing(assignment)) {
return "Mirror sync in progress";
}
if (assignment.lastCopyStatus === "error" && assignment.lastCopyError) { if (assignment.lastCopyStatus === "error" && assignment.lastCopyError) {
return assignment.lastCopyError; return assignment.lastCopyError;
} }
@ -310,19 +352,14 @@ export const ScheduleMirrorsConfig = ({ scheduleId, primaryRepositoryId, reposit
/> />
</TableCell> </TableCell>
<TableCell> <TableCell>
{assignment.lastCopyAt ? ( <div className="flex items-center gap-2">
<div className="flex items-center gap-2"> <StatusDot
<StatusDot variant={getStatusVariant(assignment.lastCopyStatus)}
variant={getStatusVariant(assignment.lastCopyStatus)} label={getStatusLabel(assignment)}
label={getStatusLabel(assignment)} animated={isSyncing(assignment) ? true : undefined}
/> />
<span className="text-sm text-muted-foreground"> <span className="text-sm text-muted-foreground">{getLabel(assignment)}</span>
{formatDistanceToNow(new Date(assignment.lastCopyAt), { addSuffix: true })} </div>
</span>
</div>
) : (
<span className="text-sm text-muted-foreground">Never</span>
)}
</TableCell> </TableCell>
<TableCell> <TableCell>
<Button <Button

View file

@ -1,4 +1,3 @@
import { useEffect } from "react";
import type { ListSnapshotsResponse } from "~/client/api-client"; import type { ListSnapshotsResponse } from "~/client/api-client";
import { ByteSize } from "~/client/components/bytes-size"; import { ByteSize } from "~/client/components/bytes-size";
import { Card, CardContent } from "~/client/components/ui/card"; import { Card, CardContent } from "~/client/components/ui/card";
@ -17,12 +16,6 @@ interface Props {
export const SnapshotTimeline = (props: Props) => { export const SnapshotTimeline = (props: Props) => {
const { snapshots, snapshotId, loading, onSnapshotSelect, error } = props; const { snapshots, snapshotId, loading, onSnapshotSelect, error } = props;
useEffect(() => {
if (!snapshotId && snapshots.length > 0) {
onSnapshotSelect(snapshots[snapshots.length - 1].short_id);
}
}, [snapshotId, snapshots, onSnapshotSelect]);
if (error) { if (error) {
return ( return (
<Card> <Card>

View file

@ -10,7 +10,7 @@ import {
} from "@dnd-kit/core"; } from "@dnd-kit/core";
import { arrayMove, SortableContext, sortableKeyboardCoordinates, rectSortingStrategy } from "@dnd-kit/sortable"; import { arrayMove, SortableContext, sortableKeyboardCoordinates, rectSortingStrategy } from "@dnd-kit/sortable";
import { CalendarClock, Plus } from "lucide-react"; import { CalendarClock, Plus } from "lucide-react";
import { useState, useEffect } from "react"; import { useState } from "react";
import { EmptyState } from "~/client/components/empty-state"; import { EmptyState } from "~/client/components/empty-state";
import { Button } from "~/client/components/ui/button"; import { Button } from "~/client/components/ui/button";
import { Card, CardContent } from "~/client/components/ui/card"; import { Card, CardContent } from "~/client/components/ui/card";
@ -27,12 +27,8 @@ export function BackupsPage() {
...listBackupSchedulesOptions(), ...listBackupSchedulesOptions(),
}); });
const [items, setItems] = useState(schedules?.map((s) => s.id) ?? []); const [localItems, setLocalItems] = useState<number[] | null>(null);
useEffect(() => { const items = localItems ?? schedules?.map((s) => s.id) ?? [];
if (schedules) {
setItems(schedules.map((s) => s.id));
}
}, [schedules]);
const sensors = useSensors( const sensors = useSensors(
useSensor(PointerSensor, { useSensor(PointerSensor, {
@ -51,10 +47,11 @@ export function BackupsPage() {
const { active, over } = event; const { active, over } = event;
if (over && active.id !== over.id) { if (over && active.id !== over.id) {
setItems((items) => { setLocalItems((currentItems) => {
const oldIndex = items.indexOf(active.id as number); const baseItems = currentItems ?? schedules?.map((s) => s.id) ?? [];
const newIndex = items.indexOf(over.id as number); const oldIndex = baseItems.indexOf(active.id as number);
const newItems = arrayMove(items, oldIndex, newIndex); const newIndex = baseItems.indexOf(over.id as number);
const newItems = arrayMove(baseItems, oldIndex, newIndex);
reorderMutation.mutate({ body: { scheduleIds: newItems } }); reorderMutation.mutate({ body: { scheduleIds: newItems } });
return newItems; return newItems;

View file

@ -1,6 +1,5 @@
import { arktypeResolver } from "@hookform/resolvers/arktype"; import { arktypeResolver } from "@hookform/resolvers/arktype";
import { type } from "arktype"; import { type } from "arktype";
import { useEffect } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { cn } from "~/client/lib/utils"; import { cn } from "~/client/lib/utils";
import { deepClean } from "~/utils/object"; import { deepClean } from "~/utils/object";
@ -119,15 +118,6 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
const { watch } = form; const { watch } = form;
const watchedType = watch("type"); const watchedType = watch("type");
useEffect(() => {
if (!initialValues) {
form.reset({
name: form.getValues().name || "",
...defaultValuesForType[watchedType as keyof typeof defaultValuesForType],
});
}
}, [watchedType, form, initialValues]);
return ( return (
<Form {...form}> <Form {...form}>
<form id={formId} onSubmit={form.handleSubmit(onSubmit)} className={cn("space-y-4", className)}> <form id={formId} onSubmit={form.handleSubmit(onSubmit)} className={cn("space-y-4", className)}>
@ -138,12 +128,7 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
<FormItem> <FormItem>
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} placeholder="My notification" max={32} min={2} />
{...field}
placeholder="My notification"
max={32}
min={2}
/>
</FormControl> </FormControl>
<FormDescription>Unique identifier for this notification destination.</FormDescription> <FormDescription>Unique identifier for this notification destination.</FormDescription>
<FormMessage /> <FormMessage />
@ -158,7 +143,15 @@ export const CreateNotificationForm = ({ onSubmit, mode = "create", initialValue
<FormItem> <FormItem>
<FormLabel>Type</FormLabel> <FormLabel>Type</FormLabel>
<Select <Select
onValueChange={field.onChange} onValueChange={(value) => {
field.onChange(value);
if (!initialValues) {
form.reset({
name: form.getValues().name || "",
...defaultValuesForType[value as keyof typeof defaultValuesForType],
});
}
}}
value={field.value} value={field.value}
disabled={mode === "update"} disabled={mode === "update"}
> >

View file

@ -1,6 +1,6 @@
import { arktypeResolver } from "@hookform/resolvers/arktype"; import { arktypeResolver } from "@hookform/resolvers/arktype";
import { type } from "arktype"; import { type } from "arktype";
import { useEffect, useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { Save } from "lucide-react"; import { Save } from "lucide-react";
import { cn } from "~/client/lib/utils"; import { cn } from "~/client/lib/utils";
@ -88,15 +88,6 @@ export const CreateRepositoryForm = ({
const { capabilities } = useSystemInfo(); const { capabilities } = useSystemInfo();
useEffect(() => {
form.reset({
name: form.getValues().name,
isExistingRepository: form.getValues().isExistingRepository,
customPassword: form.getValues().customPassword,
...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType],
});
}, [watchedBackend, form]);
return ( return (
<Form {...form}> <Form {...form}>
<form id={formId} onSubmit={form.handleSubmit(onSubmit)} className={cn("space-y-4", className)}> <form id={formId} onSubmit={form.handleSubmit(onSubmit)} className={cn("space-y-4", className)}>
@ -126,7 +117,18 @@ export const CreateRepositoryForm = ({
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Backend</FormLabel> <FormLabel>Backend</FormLabel>
<Select onValueChange={field.onChange} value={field.value}> <Select
onValueChange={(value) => {
field.onChange(value);
form.reset({
name: form.getValues().name,
isExistingRepository: form.getValues().isExistingRepository,
customPassword: form.getValues().customPassword,
...defaultValuesForType[value as keyof typeof defaultValuesForType],
});
}}
value={field.value}
>
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a backend" /> <SelectValue placeholder="Select a backend" />

View file

@ -1,14 +1,19 @@
import { useQueryClient, useSuspenseQuery } from "@tanstack/react-query"; import { useSuspenseQuery } from "@tanstack/react-query";
import { Suspense, useEffect } from "react"; import { Suspense } from "react";
import { getRepositoryOptions, listSnapshotsOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { getRepositoryOptions } from "~/client/api-client/@tanstack/react-query.gen";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "~/client/components/ui/tabs";
import { RepositoryInfoTabContent } from "../tabs/info"; import { RepositoryInfoTabContent } from "../tabs/info";
import { RepositorySnapshotsTabContent } from "../tabs/snapshots"; import { RepositorySnapshotsTabContent } from "../tabs/snapshots";
import { useNavigate, useSearch } from "@tanstack/react-router"; import { useNavigate, useSearch } from "@tanstack/react-router";
export default function RepositoryDetailsPage({ repositoryId }: { repositoryId: string }) { export const handle = {
const queryClient = useQueryClient(); breadcrumb: (match: Route.MetaArgs) => [
{ label: "Repositories", href: "/repositories" },
{ label: match.loaderData?.name || match.params.id },
],
};
export default function RepositoryDetailsPage({ repositoryId }: { repositoryId: string }) {
const navigate = useNavigate(); const navigate = useNavigate();
const { tab } = useSearch({ from: "/(dashboard)/repositories/$repositoryId" }); const { tab } = useSearch({ from: "/(dashboard)/repositories/$repositoryId" });
const activeTab = tab || "info"; const activeTab = tab || "info";
@ -17,10 +22,6 @@ export default function RepositoryDetailsPage({ repositoryId }: { repositoryId:
...getRepositoryOptions({ path: { id: repositoryId } }), ...getRepositoryOptions({ path: { id: repositoryId } }),
}); });
useEffect(() => {
void queryClient.prefetchQuery(listSnapshotsOptions({ path: { id: data.id } }));
}, [queryClient, data.id]);
return ( return (
<> <>
<Tabs value={activeTab} onValueChange={(value) => navigate({ to: ".", search: (s) => ({ ...s, tab: value }) })}> <Tabs value={activeTab} onValueChange={(value) => navigate({ to: ".", search: (s) => ({ ...s, tab: value }) })}>

View file

@ -2,7 +2,7 @@ import { arktypeResolver } from "@hookform/resolvers/arktype";
import { useMutation } from "@tanstack/react-query"; import { useMutation } from "@tanstack/react-query";
import { type } from "arktype"; import { type } from "arktype";
import { CheckCircle, Loader2, Plug, Save, XCircle } from "lucide-react"; import { CheckCircle, Loader2, Plug, Save, XCircle } from "lucide-react";
import { useEffect, useState } from "react"; import { useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { cn } from "~/client/lib/utils"; import { cn } from "~/client/lib/utils";
import { deepClean } from "~/utils/object"; import { deepClean } from "~/utils/object";
@ -62,20 +62,11 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
}, },
}); });
const { watch, getValues } = form; const { getValues, watch } = form;
const { capabilities } = useSystemInfo(); const { capabilities } = useSystemInfo();
const watchedBackend = watch("backend"); const watchedBackend = watch("backend");
useEffect(() => {
if (mode === "create") {
form.reset({
name: form.getValues().name,
...defaultValuesForType[watchedBackend as keyof typeof defaultValuesForType],
});
}
}, [watchedBackend, form, mode]);
const [testMessage, setTestMessage] = useState<{ success: boolean; message: string } | null>(null); const [testMessage, setTestMessage] = useState<{ success: boolean; message: string } | null>(null);
const testBackendConnection = useMutation({ const testBackendConnection = useMutation({
@ -119,12 +110,7 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
<FormItem> <FormItem>
<FormLabel>Name</FormLabel> <FormLabel>Name</FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} placeholder="Volume name" maxLength={32} minLength={2} />
{...field}
placeholder="Volume name"
maxLength={32}
minLength={2}
/>
</FormControl> </FormControl>
<FormDescription>Unique identifier for the volume.</FormDescription> <FormDescription>Unique identifier for the volume.</FormDescription>
<FormMessage /> <FormMessage />
@ -138,7 +124,18 @@ export const CreateVolumeForm = ({ onSubmit, mode = "create", initialValues, for
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Backend</FormLabel> <FormLabel>Backend</FormLabel>
<Select onValueChange={field.onChange} value={field.value}> <Select
onValueChange={(value) => {
field.onChange(value);
if (mode === "create") {
form.reset({
name: form.getValues().name,
...defaultValuesForType[value as keyof typeof defaultValuesForType],
});
}
}}
value={field.value}
>
<FormControl> <FormControl>
<SelectTrigger> <SelectTrigger>
<SelectValue placeholder="Select a backend" /> <SelectValue placeholder="Select a backend" />

View file

@ -346,7 +346,7 @@ export const backupScheduleMirrorsTable = sqliteTable(
.references(() => repositoriesTable.id, { onDelete: "cascade" }), .references(() => repositoriesTable.id, { onDelete: "cascade" }),
enabled: int("enabled", { mode: "boolean" }).notNull().default(true), enabled: int("enabled", { mode: "boolean" }).notNull().default(true),
lastCopyAt: int("last_copy_at", { mode: "number" }), lastCopyAt: int("last_copy_at", { mode: "number" }),
lastCopyStatus: text("last_copy_status").$type<"success" | "error">(), lastCopyStatus: text("last_copy_status").$type<"success" | "error" | "in_progress">(),
lastCopyError: text("last_copy_error"), lastCopyError: text("last_copy_error"),
createdAt: int("created_at", { mode: "number" }) createdAt: int("created_at", { mode: "number" })
.notNull() .notNull()

View file

@ -46,7 +46,7 @@ const scheduleMirrorSchema = type({
repositoryId: "string", repositoryId: "string",
enabled: "boolean", enabled: "boolean",
lastCopyAt: "number | null", lastCopyAt: "number | null",
lastCopyStatus: "'success' | 'error' | null", lastCopyStatus: "'success' | 'error' | 'in_progress' | null",
lastCopyError: "string | null", lastCopyError: "string | null",
createdAt: "number", createdAt: "number",
repository: repositorySchema, repository: repositorySchema,

View file

@ -384,6 +384,11 @@ const copyToSingleMirror = async (
repositoryName: mirror.repository.name, repositoryName: mirror.repository.name,
}); });
await mirrorQueries.updateStatus(mirror.id, {
lastCopyStatus: "in_progress",
lastCopyError: null,
});
const releaseSource = await repoMutex.acquireShared(sourceRepository.id, `mirror_source:${scheduleId}`); const releaseSource = await repoMutex.acquireShared(sourceRepository.id, `mirror_source:${scheduleId}`);
const releaseMirror = await repoMutex.acquireShared(mirror.repository.id, `mirror:${scheduleId}`); const releaseMirror = await repoMutex.acquireShared(mirror.repository.id, `mirror:${scheduleId}`);

View file

@ -3,7 +3,13 @@ import { db } from "../../db/db";
import { backupSchedulesTable, backupScheduleMirrorsTable } from "../../db/schema"; import { backupSchedulesTable, backupScheduleMirrorsTable } from "../../db/schema";
export type BackupStatusType = "in_progress" | "success" | "warning" | "error"; export type BackupStatusType = "in_progress" | "success" | "warning" | "error";
export type MirrorStatusType = "success" | "error"; export type MirrorStatusType = "in_progress" | "success" | "error";
type MirrorStatusUpdate = {
lastCopyAt?: number | null;
lastCopyStatus?: MirrorStatusType;
lastCopyError?: string | null;
};
export const scheduleQueries = { export const scheduleQueries = {
findById: async (scheduleId: number, organizationId: string) => { findById: async (scheduleId: number, organizationId: string) => {
@ -53,14 +59,7 @@ export const mirrorQueries = {
}); });
}, },
updateStatus: async ( updateStatus: async (mirrorId: number, status: MirrorStatusUpdate) => {
mirrorId: number,
status: {
lastCopyAt: number;
lastCopyStatus: MirrorStatusType;
lastCopyError: string | null;
},
) => {
return db.update(backupScheduleMirrorsTable).set(status).where(eq(backupScheduleMirrorsTable.id, mirrorId)); return db.update(backupScheduleMirrorsTable).set(status).where(eq(backupScheduleMirrorsTable.id, mirrorId));
}, },
}; };