fix: also add for restoration

This commit is contained in:
Charles GTE
2026-06-07 11:15:21 +02:00
parent 2932149b92
commit 02a8f0baf9
11 changed files with 3056 additions and 28 deletions
@@ -59,6 +59,9 @@ export default async function RoutePage(props: PageParams<{
const restorations = await db.query.restoration.findMany({ const restorations = await db.query.restoration.findMany({
where: eq(drizzleDb.schemas.restoration.databaseId, dbItem.id), where: eq(drizzleDb.schemas.restoration.databaseId, dbItem.id),
with: {
logs: true
},
orderBy: (r, {desc}) => [desc(r.createdAt)], orderBy: (r, {desc}) => [desc(r.createdAt)],
}); });
+31 -5
View File
@@ -1,17 +1,20 @@
import {NextResponse} from "next/server"; import {NextResponse} from "next/server";
import {isUuidv4} from "@/utils/verify-uuid"; import {isUuidv4} from "@/utils/verify-uuid";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {db} from "@/db"; import {db as dbClient, db} from "@/db";
import {and, eq} from "drizzle-orm"; import {and, eq} from "drizzle-orm";
import {sendNotificationsBackupRestore} from "@/features/notifications/notifications.helpers"; import {sendNotificationsBackupRestore} from "@/features/notifications/notifications.helpers";
import {logger} from "@/lib/logger"; import {logger} from "@/lib/logger";
import {withUpdatedAt} from "@/db/utils"; import {withUpdatedAt} from "@/db/utils";
import {JobLogEntry} from "@/features/logs/types";
const log = logger.child({module: "api/agent/restore"}); const log = logger.child({module: "api/agent/restore"});
export type BodyResultRestore = { export type BodyResultRestore = {
generatedId: string generatedId: string
status: string status: string
logs: JobLogEntry[]
durationMs: number
} }
type RestorationStatus = 'waiting' | 'ongoing' | 'failed' | 'success'; type RestorationStatus = 'waiting' | 'ongoing' | 'failed' | 'success';
@@ -60,11 +63,34 @@ export async function POST(
return NextResponse.json({error: "Unable to fin the corresponding restoration"}, {status: 404}) return NextResponse.json({error: "Unable to fin the corresponding restoration"}, {status: 404})
} }
const [restorationUpdated] = await db
await db
.update(drizzleDb.schemas.restoration) .update(drizzleDb.schemas.restoration)
.set(withUpdatedAt({status: body.status as RestorationStatus})) .set(withUpdatedAt({status: body.status as RestorationStatus, durationMs: body.durationMs}))
.where(eq(drizzleDb.schemas.restoration.id, restoration.id)); .where(eq(drizzleDb.schemas.restoration.id, restoration.id)).returning();
const logsToInsert = body.logs.map((entry) => ({
backupId: null,
restorationId: restorationUpdated.id,
loggedAt: new Date(entry.timestamp),
entryType: entry.type,
level: entry.level,
message: entry.message,
command: entry.command ?? null,
output: entry.output ?? null,
exitCode: entry.exit_code ?? null,
durationMs: entry.duration_ms ?? null,
}));
if (logsToInsert.length > 0) {
await dbClient
.insert(drizzleDb.schemas.jobLog)
.values(logsToInsert);
}
await sendNotificationsBackupRestore(database, body.status == "failed" ? "error_restore" : "success_restore"); await sendNotificationsBackupRestore(database, body.status == "failed" ? "error_restore" : "success_restore");
@@ -0,0 +1 @@
ALTER TABLE "restorations" ADD COLUMN "duration_ms" bigint;
File diff suppressed because it is too large Load Diff
+7
View File
@@ -428,6 +428,13 @@
"when": 1780771191196, "when": 1780771191196,
"tag": "0060_shiny_sersi", "tag": "0060_shiny_sersi",
"breakpoints": true "breakpoints": true
},
{
"idx": 61,
"version": "7",
"when": 1780822414802,
"tag": "0061_illegal_mole_man",
"breakpoints": true
} }
] ]
} }
+5 -3
View File
@@ -68,7 +68,7 @@ export const retentionPolicy = pgTable("retention_policies", {
export const restoration = pgTable("restorations", { export const restoration = pgTable("restorations", {
id: uuid("id").primaryKey().defaultRandom(), id: uuid("id").primaryKey().defaultRandom(),
status: statusEnum("status").default("waiting").notNull(), status: statusEnum("status").default("waiting").notNull(),
durationMs: bigint("duration_ms", { mode: "number" }),
backupStorageId: uuid("backup_storage_id") backupStorageId: uuid("backup_storage_id")
.references(() => backupStorage.id, {onDelete: "cascade"}), .references(() => backupStorage.id, {onDelete: "cascade"}),
backupId: uuid("backup_id") backupId: uuid("backup_id")
@@ -136,8 +136,6 @@ export type DatabaseWith = Database & {
retentionPolicy?: RetentionPolicy | null; retentionPolicy?: RetentionPolicy | null;
alertPolicies?: AlertPolicy[] | null; alertPolicies?: AlertPolicy[] | null;
storagePolicies?: StoragePolicy[] | null; storagePolicies?: StoragePolicy[] | null;
logs?: JobLog[] | null;
}; };
@@ -145,7 +143,11 @@ export type BackupWith = Backup & {
restorations?: Restoration[] | null; restorations?: Restoration[] | null;
storages?: BackupStorage[] | null; storages?: BackupStorage[] | null;
logs?: JobLog[] | null; logs?: JobLog[] | null;
};
export type RestorationWith = Restoration & {
logs?: JobLog[] | null;
}; };
@@ -4,7 +4,7 @@ import {z} from "zod";
import {db} from "@/db"; import {db} from "@/db";
import {eq} from "drizzle-orm"; import {eq} from "drizzle-orm";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {BackupWith, Restoration} from "@/db/schema/07_database"; import {BackupWith, Restoration, RestorationWith} from "@/db/schema/07_database";
import {getOrganizationChannels} from "@/db/services/notification-channel"; import {getOrganizationChannels} from "@/db/services/notification-channel";
import {getOrganizationStorageChannels} from "@/db/services/storage-channel"; import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
import {getHealthLast12hLogs} from "@/db/services/healthcheck"; import {getHealthLast12hLogs} from "@/db/services/healthcheck";
@@ -44,8 +44,11 @@ export const getDatabaseDataAction = userAction
const restorations = await db.query.restoration.findMany({ const restorations = await db.query.restoration.findMany({
where: eq(drizzleDb.schemas.restoration.databaseId, databaseId), where: eq(drizzleDb.schemas.restoration.databaseId, databaseId),
with: {
logs: true
},
orderBy: (r, {desc}) => [desc(r.createdAt)], orderBy: (r, {desc}) => [desc(r.createdAt)],
}) as Restoration[]; }) as RestorationWith[];
const totalBackups = backups.length; const totalBackups = backups.length;
const availableBackups = backups.filter(b => !b.deletedAt).length; const availableBackups = backups.filter(b => !b.deletedAt).length;
+2 -2
View File
@@ -2,7 +2,7 @@
import {DatabaseBackupActionsModal} from "@/features/database/backup-actions-modal"; import {DatabaseBackupActionsModal} from "@/features/database/backup-actions-modal";
import {DatabaseTabs} from "@/features/database/database-tabs"; import {DatabaseTabs} from "@/features/database/database-tabs";
import {Setting} from "@/db/schema/01_setting"; import {Setting} from "@/db/schema/01_setting";
import {BackupWith, DatabaseWith, Restoration} from "@/db/schema/07_database"; import {BackupWith, DatabaseWith, RestorationWith} from "@/db/schema/07_database";
import {MemberWithUser} from "@/db/schema/03_organization"; import {MemberWithUser} from "@/db/schema/03_organization";
import {useBackupModal} from "@/features/database/backup-modal-context"; import {useBackupModal} from "@/features/database/backup-modal-context";
import {DatabaseKpi} from "@/features/database/database-kpi"; import {DatabaseKpi} from "@/features/database/database-kpi";
@@ -28,7 +28,7 @@ import {LogsModal} from "@/features/logs/logs-modal";
export type DatabaseContentProps = { export type DatabaseContentProps = {
settings: Setting; settings: Setting;
backups: BackupWith[]; backups: BackupWith[];
restorations: Restoration[]; restorations: RestorationWith[];
isAlreadyRestore: boolean; isAlreadyRestore: boolean;
database: DatabaseWith; database: DatabaseWith;
activeMember: MemberWithUser; activeMember: MemberWithUser;
+20 -2
View File
@@ -13,7 +13,7 @@ import { Button } from "@/components/ui/button";
import { Check, MoreHorizontal, Trash2, X } from "lucide-react"; import { Check, MoreHorizontal, Trash2, X } from "lucide-react";
import { ReloadIcon } from "@radix-ui/react-icons"; import { ReloadIcon } from "@radix-ui/react-icons";
import { StatusBadge } from "@/components/common/status-badge"; import { StatusBadge } from "@/components/common/status-badge";
import { Restoration } from "@/db/schema/07_database"; import {Restoration, RestorationWith} from "@/db/schema/07_database";
import { formatLocalizedDate } from "@/utils/date-formatting"; import { formatLocalizedDate } from "@/utils/date-formatting";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { import {
@@ -24,11 +24,13 @@ import { toast } from "sonner";
import { TooltipCustom } from "@/components/common/tooltip-custom"; import { TooltipCustom } from "@/components/common/tooltip-custom";
import { MemberWithUser } from "@/db/schema/03_organization"; import { MemberWithUser } from "@/db/schema/03_organization";
import { ButtonWithConfirm } from "@/components/common/button-with-confirm"; import { ButtonWithConfirm } from "@/components/common/button-with-confirm";
import {LogsModalTrigger} from "@/features/logs/logs-modal-trigger";
import {formatDuration} from "@/utils/text";
export function restoreColumns( export function restoreColumns(
isAlreadyRestore: boolean, isAlreadyRestore: boolean,
activeMember: MemberWithUser, activeMember: MemberWithUser,
): ColumnDef<Restoration>[] { ): ColumnDef<RestorationWith>[] {
return [ return [
{ {
accessorKey: "id", accessorKey: "id",
@@ -48,6 +50,22 @@ export function restoreColumns(
return <StatusBadge status={row.getValue("status")} />; return <StatusBadge status={row.getValue("status")} />;
}, },
}, },
{
accessorKey: "durationMs",
header: "Duration",
cell: ({row}) => {
const durationMs = row.getValue("durationMs");
return durationMs ? formatDuration(row.getValue("durationMs")) : "-"
},
},
{
accessorKey: "logs",
header: "Logs",
cell: ({row}) => {
const logs = row.original.logs ?? [];
return <LogsModalTrigger logs={logs}/>;
},
},
{ {
id: "actions", id: "actions",
cell: ({ row }) => { cell: ({ row }) => {
+7 -2
View File
@@ -21,6 +21,9 @@ export const deleteRestoreAction = userAction
.where(and(eq(drizzleDb.schemas.restoration.id, parsedInput.restorationId))) .where(and(eq(drizzleDb.schemas.restoration.id, parsedInput.restorationId)))
.execute(); .execute();
await db
.delete(drizzleDb.schemas.jobLog)
.where(eq(drizzleDb.schemas.jobLog.restorationId, parsedInput.restorationId));
return { return {
success: true, success: true,
@@ -50,14 +53,16 @@ export const rerunRestorationAction = userAction
) )
.action(async ({parsedInput}): Promise<ServerActionResult<Restoration>> => { .action(async ({parsedInput}): Promise<ServerActionResult<Restoration>> => {
try { try {
const updateResult = await db const [updatedRestoration] = await db
.update(drizzleDb.schemas.restoration) .update(drizzleDb.schemas.restoration)
.set({status: "waiting"}) .set({status: "waiting"})
.where(eq(drizzleDb.schemas.restoration.id, parsedInput.restorationId)) .where(eq(drizzleDb.schemas.restoration.id, parsedInput.restorationId))
.returning() .returning()
.execute(); .execute();
const updatedRestoration = updateResult[0]; await db
.delete(drizzleDb.schemas.jobLog)
.where(eq(drizzleDb.schemas.jobLog.restorationId, parsedInput.restorationId));
if (!updatedRestoration) { if (!updatedRestoration) {
return { return {
+8 -12
View File
@@ -3,6 +3,7 @@
import { useState } from "react" import { useState } from "react"
import { Minus, Plus } from "lucide-react" import { Minus, Plus } from "lucide-react"
import { cn } from "@/lib/utils" import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
import { LevelType } from "@/features/logs/types" import { LevelType } from "@/features/logs/types"
import { JobLog } from "@/db/schema/17_job-log" import { JobLog } from "@/db/schema/17_job-log"
import { formatLocalizedDate } from "@/utils/date-formatting" import { formatLocalizedDate } from "@/utils/date-formatting"
@@ -19,24 +20,19 @@ function TypeBadge({ entry }: { entry: JobLog }) {
const isCommand = entry.entryType === "command" const isCommand = entry.entryType === "command"
const label = isCommand ? "Command" : levelLabel[entry.level] const label = isCommand ? "Command" : levelLabel[entry.level]
const styles = isCommand const styles = isCommand
? "bg-secondary text-secondary-foreground" ? "border-transparent bg-secondary text-secondary-foreground"
: entry.level === "error" : entry.level === "error"
? "bg-destructive/20 text-destructive" ? "border-transparent bg-destructive/20 text-destructive"
: entry.level === "warn" : entry.level === "warn"
? "bg-amber-500/20 text-amber-600 dark:text-amber-300" ? "border-transparent bg-amber-500/20 text-amber-600 dark:text-amber-300"
: entry.level === "debug" : entry.level === "debug"
? "bg-muted text-muted-foreground" ? "border-transparent bg-muted text-muted-foreground"
: "bg-sky-500/20 text-sky-600 dark:text-sky-300" : "border-transparent bg-sky-500/20 text-sky-600 dark:text-sky-300"
return ( return (
<span <Badge variant="outline" className={cn("rounded-full", styles)}>
className={cn(
"inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium",
styles,
)}
>
{label} {label}
</span> </Badge>
) )
} }