diff --git a/apps/client/app/api-client/types.gen.ts b/apps/client/app/api-client/types.gen.ts index 184207f..6e7b018 100644 --- a/apps/client/app/api-client/types.gen.ts +++ b/apps/client/app/api-client/types.gen.ts @@ -256,7 +256,8 @@ export type GetVolumeResponse = GetVolumeResponses[keyof GetVolumeResponses]; export type UpdateVolumeData = { body?: { - config: + autoRemount?: boolean; + config?: | { backend: "directory"; } @@ -308,6 +309,7 @@ export type UpdateVolumeResponses = { 200: { message: string; volume: { + autoRemount: boolean; config: | { backend: "directory"; @@ -339,9 +341,12 @@ export type UpdateVolumeResponses = { username?: string; }; createdAt: number; + lastError: string; + lastHealthCheck: number; name: string; path: string; - type: string; + status: "error" | "mounted" | "unknown" | "unmounted"; + type: "directory" | "nfs" | "smb" | "webdav"; updatedAt: number; }; }; diff --git a/apps/client/app/components/onoff.tsx b/apps/client/app/components/onoff.tsx index 90089fd..1ee4a6f 100644 --- a/apps/client/app/components/onoff.tsx +++ b/apps/client/app/components/onoff.tsx @@ -6,9 +6,10 @@ type Props = { toggle: (v: boolean) => void; enabledLabel: string; disabledLabel: string; + disabled?: boolean; }; -export const OnOff = ({ isOn, toggle, enabledLabel, disabledLabel }: Props) => { +export const OnOff = ({ isOn, toggle, enabledLabel, disabledLabel, disabled }: Props) => { return (
{ )} > {isOn ? enabledLabel : disabledLabel} - +
); }; diff --git a/apps/client/app/modules/details/components/healthchecks-card.tsx b/apps/client/app/modules/details/components/healthchecks-card.tsx index a7cbf36..c76035a 100644 --- a/apps/client/app/modules/details/components/healthchecks-card.tsx +++ b/apps/client/app/modules/details/components/healthchecks-card.tsx @@ -2,7 +2,7 @@ import { useMutation } from "@tanstack/react-query"; import { formatDistanceToNow } from "date-fns"; import { HeartIcon } from "lucide-react"; import { toast } from "sonner"; -import { healthCheckVolumeMutation } from "~/api-client/@tanstack/react-query.gen"; +import { healthCheckVolumeMutation, updateVolumeMutation } from "~/api-client/@tanstack/react-query.gen"; import { OnOff } from "~/components/onoff"; import { Button } from "~/components/ui/button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card"; @@ -28,6 +28,15 @@ export const HealthchecksCard = ({ volume }: Props) => { }, }); + const toggleAutoRemount = useMutation({ + ...updateVolumeMutation(), + onSuccess: (d) => { + toast.success("Volume updated", { + description: `Auto remount is now ${d.volume.autoRemount ? "enabled" : "paused"}.`, + }); + }, + }); + return ( @@ -46,7 +55,15 @@ export const HealthchecksCard = ({ volume }: Props) => { )} Remount on error - {}} enabledLabel="Enabled" disabledLabel="Paused" /> + + toggleAutoRemount.mutate({ path: { name: volume.name }, body: { autoRemount: !volume.autoRemount } }) + } + disabled={toggleAutoRemount.isPending} + enabledLabel="Enabled" + disabledLabel="Paused" + /> {volume.status !== "unmounted" && ( diff --git a/apps/server/src/modules/lifecycle/startup.ts b/apps/server/src/modules/lifecycle/startup.ts index b6125a2..17d55e4 100644 --- a/apps/server/src/modules/lifecycle/startup.ts +++ b/apps/server/src/modules/lifecycle/startup.ts @@ -9,7 +9,7 @@ export const startup = async () => { const volumes = await db.query.volumesTable.findMany({ where: or( eq(volumesTable.status, "mounted"), - and(eq(volumesTable.autoRemount, 1), eq(volumesTable.status, "error")), + and(eq(volumesTable.autoRemount, true), eq(volumesTable.status, "error")), ), }); @@ -28,10 +28,8 @@ export const startup = async () => { }); for (const volume of volumes) { - const { error } = await volumeService.checkHealth(volume.name); - if (error && volume.autoRemount) { - // TODO: retry with backoff based on last health check time - // Until we reach the max backoff and it'll try every 10 minutes + const { status } = await volumeService.checkHealth(volume.name); + if (status === "error" && volume.autoRemount) { await volumeService.mountVolume(volume.name); } } diff --git a/apps/server/src/modules/volumes/volume.controller.ts b/apps/server/src/modules/volumes/volume.controller.ts index d1a47eb..4d4d99a 100644 --- a/apps/server/src/modules/volumes/volume.controller.ts +++ b/apps/server/src/modules/volumes/volume.controller.ts @@ -17,6 +17,7 @@ import { updateVolumeBody, updateVolumeDto, healthCheckDto, + type UpdateVolumeResponseDto, } from "./volume.dto"; import { volumeService } from "./volume.service"; @@ -86,19 +87,17 @@ export const volumeController = new Hono() .put("/:name", updateVolumeDto, validator("json", updateVolumeBody), async (c) => { const { name } = c.req.param(); const body = c.req.valid("json"); - const res = await volumeService.updateVolume(name, body.config); + const res = await volumeService.updateVolume(name, body); const response = { message: "Volume updated", volume: { - name: res.volume.name, - path: res.volume.path, - type: res.volume.type, + ...res.volume, createdAt: res.volume.createdAt.getTime(), updatedAt: res.volume.updatedAt.getTime(), - config: res.volume.config, + lastHealthCheck: res.volume.lastHealthCheck.getTime(), }, - }; + } satisfies UpdateVolumeResponseDto; return c.json(response, 200); }) diff --git a/apps/server/src/modules/volumes/volume.dto.ts b/apps/server/src/modules/volumes/volume.dto.ts index 445275b..c139963 100644 --- a/apps/server/src/modules/volumes/volume.dto.ts +++ b/apps/server/src/modules/volumes/volume.dto.ts @@ -139,19 +139,15 @@ export const getVolumeDto = describeRoute({ * Update a volume */ export const updateVolumeBody = type({ - config: volumeConfigSchema, + autoRemount: "boolean?", + config: volumeConfigSchema.optional(), }); +export type UpdateVolumeBody = typeof updateVolumeBody.infer; + export const updateVolumeResponse = type({ message: "string", - volume: type({ - name: "string", - path: "string", - type: "string", - createdAt: "number", - updatedAt: "number", - config: volumeConfigSchema, - }), + volume: volumeSchema, }); export const updateVolumeDto = describeRoute({ @@ -174,6 +170,8 @@ export const updateVolumeDto = describeRoute({ }, }); +export type UpdateVolumeResponseDto = typeof updateVolumeResponse.infer; + /** * Test connection */ diff --git a/apps/server/src/modules/volumes/volume.service.ts b/apps/server/src/modules/volumes/volume.service.ts index ea16f53..96a5dbc 100644 --- a/apps/server/src/modules/volumes/volume.service.ts +++ b/apps/server/src/modules/volumes/volume.service.ts @@ -13,6 +13,7 @@ import { volumesTable } from "../../db/schema"; import { toMessage } from "../../utils/errors"; import { getStatFs, type StatFs } from "../../utils/mountinfo"; import { createVolumeBackend } from "../backends/backend"; +import type { UpdateVolumeBody } from "./volume.dto"; const listVolumes = async () => { const volumes = await db.query.volumesTable.findMany({}); @@ -114,7 +115,7 @@ const getVolume = async (name: string) => { return { volume, statfs }; }; -const updateVolume = async (name: string, backendConfig: BackendConfig) => { +const updateVolume = async (name: string, volumeData: UpdateVolumeBody) => { const existing = await db.query.volumesTable.findFirst({ where: eq(volumesTable.name, name), }); @@ -126,10 +127,10 @@ const updateVolume = async (name: string, backendConfig: BackendConfig) => { const [updated] = await db .update(volumesTable) .set({ - config: backendConfig, - type: backendConfig.backend, + config: volumeData.config, + type: volumeData.config?.backend, + autoRemount: volumeData.autoRemount, updatedAt: new Date(), - status: "unmounted", }) .where(eq(volumesTable.name, name)) .returning();