feat: toggle auto remount

This commit is contained in:
Nicolas Meienberger
2025-09-27 11:22:47 +02:00
parent 7154dcdbac
commit 35779b5ce3
7 changed files with 49 additions and 30 deletions

View File

@@ -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;
};
};

View File

@@ -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 (
<div
className={cn(
@@ -19,7 +20,7 @@ export const OnOff = ({ isOn, toggle, enabledLabel, disabledLabel }: Props) => {
)}
>
<span>{isOn ? enabledLabel : disabledLabel}</span>
<Switch checked={isOn} onCheckedChange={toggle} />
<Switch disabled={disabled} checked={isOn} onCheckedChange={toggle} />
</div>
);
};

View File

@@ -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 (
<Card className="flex-1 flex flex-col h-full">
<CardHeader>
@@ -46,7 +55,15 @@ export const HealthchecksCard = ({ volume }: Props) => {
)}
<span className="flex justify-between items-center gap-2">
<span className="text-sm">Remount on error</span>
<OnOff isOn={volume.autoRemount} toggle={() => {}} enabledLabel="Enabled" disabledLabel="Paused" />
<OnOff
isOn={volume.autoRemount}
toggle={() =>
toggleAutoRemount.mutate({ path: { name: volume.name }, body: { autoRemount: !volume.autoRemount } })
}
disabled={toggleAutoRemount.isPending}
enabledLabel="Enabled"
disabledLabel="Paused"
/>
</span>
</div>
{volume.status !== "unmounted" && (

View File

@@ -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);
}
}

View File

@@ -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);
})

View File

@@ -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
*/

View File

@@ -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();