refactor: pr feedbacks

This commit is contained in:
Nicolas Meienberger 2026-02-12 18:18:30 +01:00
parent 7f1874a571
commit 40a1bf5e3a
5 changed files with 62 additions and 71 deletions

View file

@ -5,7 +5,7 @@ import { Progress } from "~/client/components/ui/progress";
import { useServerEvents } from "~/client/hooks/use-server-events"; import { useServerEvents } from "~/client/hooks/use-server-events";
import type { BackupProgressEventDto } from "~/schemas/events-dto"; import type { BackupProgressEventDto } from "~/schemas/events-dto";
import { formatDuration } from "~/utils/utils"; import { formatDuration } from "~/utils/utils";
import { formatBytes } from "../../../../utils/format-bytes"; import { formatBytes } from "~/utils/format-bytes";
type Props = { type Props = {
scheduleId: number; scheduleId: number;
@ -71,7 +71,9 @@ export const BackupProgressCard = ({ scheduleId }: Props) => {
<p className="font-medium"> <p className="font-medium">
{progress ? ( {progress ? (
<> <>
<ByteSize bytes={progress.bytes_done} base={1024} /> / <ByteSize bytes={progress.total_bytes} base={1024} /> <ByteSize bytes={progress.bytes_done} base={1024} />
&nbsp;/&nbsp;
<ByteSize bytes={progress.total_bytes} base={1024} />
</> </>
) : ( ) : (
"—" "—"

View file

@ -132,11 +132,6 @@ const runBackupOperation = async (ctx: BackupContext, signal: AbortSignal) => {
} }
}; };
const buildBackupSummary = (result: ResticBackupOutputDto | null | undefined) => {
if (!result) return undefined;
return result;
};
const finalizeSuccessfulBackup = async ( const finalizeSuccessfulBackup = async (
ctx: BackupContext, ctx: BackupContext,
scheduleId: number, scheduleId: number,
@ -144,7 +139,6 @@ const finalizeSuccessfulBackup = async (
result: ResticBackupOutputDto | null, result: ResticBackupOutputDto | null,
) => { ) => {
const finalStatus = exitCode === 0 ? "success" : "warning"; const finalStatus = exitCode === 0 ? "success" : "warning";
const summary = buildBackupSummary(result);
if (ctx.schedule.retentionPolicy) { if (ctx.schedule.retentionPolicy) {
void runForget(scheduleId).catch((error) => { void runForget(scheduleId).catch((error) => {
@ -183,7 +177,7 @@ const finalizeSuccessfulBackup = async (
volumeName: ctx.volume.name, volumeName: ctx.volume.name,
repositoryName: ctx.repository.name, repositoryName: ctx.repository.name,
status: finalStatus, status: finalStatus,
summary, summary: result ?? undefined,
}); });
notificationsService notificationsService
@ -191,7 +185,7 @@ const finalizeSuccessfulBackup = async (
volumeName: ctx.volume.name, volumeName: ctx.volume.name,
repositoryName: ctx.repository.name, repositoryName: ctx.repository.name,
scheduleName: ctx.schedule.name, scheduleName: ctx.schedule.name,
summary, summary: result ?? undefined,
}) })
.catch((error) => { .catch((error) => {
logger.error(`Failed to send backup success notification: ${toMessage(error)}`); logger.error(`Failed to send backup success notification: ${toMessage(error)}`);

View file

@ -328,7 +328,7 @@ const formatBytesText = (bytes: number) => {
return unit ? `${text} ${unit}` : text; return unit ? `${text} ${unit}` : text;
}; };
const buildBackupSummaryLines = (summary?: ResticBackupRunSummaryDto) => { const buildBackupNotificationLines = (summary?: ResticBackupRunSummaryDto) => {
if (!summary) return []; if (!summary) return [];
const safeNumber = (value: number | undefined) => (typeof value === "number" && Number.isFinite(value) ? value : 0); const safeNumber = (value: number | undefined) => (typeof value === "number" && Number.isFinite(value) ? value : 0);
@ -336,6 +336,28 @@ const buildBackupSummaryLines = (summary?: ResticBackupRunSummaryDto) => {
const safeBytesText = (value: number | undefined) => formatBytesText(safeNumber(value)); const safeBytesText = (value: number | undefined) => formatBytesText(safeNumber(value));
const safeDurationText = (value: number | undefined) => const safeDurationText = (value: number | undefined) =>
typeof value === "number" && Number.isFinite(value) ? formatDuration(Math.round(value)) : "N/A"; typeof value === "number" && Number.isFinite(value) ? formatDuration(Math.round(value)) : "N/A";
const hasDetailedStats = summary.files_new || summary.files_changed || summary.dirs_new || summary.data_blobs;
if (!hasDetailedStats) {
const lines: (string | null)[] = [];
if (summary.total_duration) {
lines.push(`Duration: ${Math.round(summary.total_duration)}s`);
}
if (summary.total_files_processed !== undefined) {
lines.push(`Files: ${summary.total_files_processed.toLocaleString()}`);
}
if (summary.total_bytes_processed !== undefined) {
lines.push(`Size: ${safeBytesText(summary.total_bytes_processed)}`);
}
if (summary.snapshot_id) {
lines.push(`Snapshot: ${summary.snapshot_id}`);
}
return lines.filter((line): line is string => Boolean(line));
}
const snapshotText = summary.snapshot_id ?? "N/A"; const snapshotText = summary.snapshot_id ?? "N/A";
const lines = [ const lines = [
@ -357,7 +379,7 @@ const buildBackupSummaryLines = (summary?: ResticBackupRunSummaryDto) => {
`- Snapshot: ${snapshotText}`, `- Snapshot: ${snapshotText}`,
]; ];
return lines.filter((line): line is string => Boolean(line)); return lines.filter(Boolean);
}; };
const sendBackupNotification = async ( const sendBackupNotification = async (
@ -368,10 +390,6 @@ const sendBackupNotification = async (
repositoryName: string; repositoryName: string;
scheduleName?: string; scheduleName?: string;
error?: string; error?: string;
duration?: number;
filesProcessed?: number;
bytesProcessed?: string;
snapshotId?: string;
summary?: ResticBackupRunSummaryDto; summary?: ResticBackupRunSummaryDto;
}, },
) => { ) => {
@ -415,11 +433,7 @@ const sendBackupNotification = async (
const decryptedConfig = await decryptSensitiveFields(assignment.destination.config); const decryptedConfig = await decryptSensitiveFields(assignment.destination.config);
const shoutrrrUrl = buildShoutrrrUrl(decryptedConfig); const shoutrrrUrl = buildShoutrrrUrl(decryptedConfig);
const result = await sendNotification({ const result = await sendNotification({ shoutrrrUrl, title, body });
shoutrrrUrl,
title,
body,
});
if (result.success) { if (result.success) {
logger.info( logger.info(
@ -448,21 +462,11 @@ function buildNotificationMessage(
repositoryName: string; repositoryName: string;
scheduleName?: string; scheduleName?: string;
error?: string; error?: string;
duration?: number;
filesProcessed?: number;
bytesProcessed?: string;
snapshotId?: string;
summary?: ResticBackupRunSummaryDto; summary?: ResticBackupRunSummaryDto;
}, },
) { ) {
const backupName = context.scheduleName ?? "backup"; const backupName = context.scheduleName ?? "backup";
const derivedDuration = const notificationLines = buildBackupNotificationLines(context.summary);
context.duration ?? (context.summary?.total_duration ? context.summary.total_duration * 1000 : undefined);
const derivedFilesProcessed = context.filesProcessed ?? context.summary?.total_files_processed;
const derivedBytesProcessed =
context.bytesProcessed ?? (context.summary ? formatBytesText(context.summary.total_bytes_processed) : undefined);
const derivedSnapshotId = context.snapshotId ?? context.summary?.snapshot_id;
const summaryLines = buildBackupSummaryLines(context.summary);
switch (event) { switch (event) {
case "start": case "start":
@ -477,40 +481,34 @@ function buildNotificationMessage(
.join("\n"), .join("\n"),
}; };
case "success": case "success": {
const bodyLines = [
`Volume: ${context.volumeName}`,
`Repository: ${context.repositoryName}`,
context.scheduleName ? `Schedule: ${context.scheduleName}` : null,
...notificationLines,
];
return { return {
title: `Zerobyte ${backupName} completed successfully`, title: `Zerobyte ${backupName} completed successfully`,
body: [ body: bodyLines.filter(Boolean).join("\n"),
`Volume: ${context.volumeName}`,
`Repository: ${context.repositoryName}`,
context.scheduleName ? `Schedule: ${context.scheduleName}` : null,
derivedDuration ? `Duration: ${Math.round(derivedDuration / 1000)}s` : null,
derivedFilesProcessed !== undefined ? `Files: ${derivedFilesProcessed.toLocaleString()}` : null,
derivedBytesProcessed ? `Size: ${derivedBytesProcessed}` : null,
derivedSnapshotId ? `Snapshot: ${derivedSnapshotId}` : null,
...summaryLines,
]
.filter(Boolean)
.join("\n"),
}; };
}
case "warning": {
const bodyLines = [
`Volume: ${context.volumeName}`,
`Repository: ${context.repositoryName}`,
context.scheduleName ? `Schedule: ${context.scheduleName}` : null,
context.error ? `Warning: ${context.error}` : null,
...notificationLines,
];
case "warning":
return { return {
title: `Zerobyte ${backupName} completed with warnings`, title: `Zerobyte ${backupName} completed with warnings`,
body: [ body: bodyLines.filter(Boolean).join("\n"),
`Volume: ${context.volumeName}`,
`Repository: ${context.repositoryName}`,
context.scheduleName ? `Schedule: ${context.scheduleName}` : null,
derivedDuration ? `Duration: ${Math.round(derivedDuration / 1000)}s` : null,
derivedFilesProcessed !== undefined ? `Files: ${derivedFilesProcessed.toLocaleString()}` : null,
derivedBytesProcessed ? `Size: ${derivedBytesProcessed}` : null,
derivedSnapshotId ? `Snapshot: ${derivedSnapshotId}` : null,
context.error ? `Warning: ${context.error}` : null,
...summaryLines,
]
.filter(Boolean)
.join("\n"),
}; };
}
case "failure": case "failure":
return { return {

View file

@ -49,6 +49,7 @@ import { getRcloneRemoteInfo, listRcloneRemotes } from "../../utils/rclone";
import { requireAuth, requireOrgAdmin } from "../auth/auth.middleware"; import { requireAuth, requireOrgAdmin } from "../auth/auth.middleware";
import { toMessage } from "~/server/utils/errors"; import { toMessage } from "~/server/utils/errors";
import { requireDevPanel } from "../auth/dev-panel.middleware"; import { requireDevPanel } from "../auth/dev-panel.middleware";
import { getSnapshotDuration } from "../../utils/snapshots";
export const repositoriesController = new Hono() export const repositoriesController = new Hono()
.use(requireAuth) .use(requireAuth)
@ -102,18 +103,14 @@ export const repositoriesController = new Hono()
const snapshots = res.map((snapshot) => { const snapshots = res.map((snapshot) => {
const { summary } = snapshot; const { summary } = snapshot;
let duration = 0; const duration = getSnapshotDuration(summary);
if (summary) {
const { backup_start, backup_end } = summary;
duration = new Date(backup_end).getTime() - new Date(backup_start).getTime();
}
return { return {
short_id: snapshot.short_id, short_id: snapshot.short_id,
duration, duration,
paths: snapshot.paths, paths: snapshot.paths,
tags: snapshot.tags ?? [], tags: snapshot.tags ?? [],
size: summary?.total_bytes_processed || 0, size: summary?.total_bytes_processed ?? 0,
time: new Date(snapshot.time).getTime(), time: new Date(snapshot.time).getTime(),
retentionCategories: retentionCategories.get(snapshot.short_id) ?? [], retentionCategories: retentionCategories.get(snapshot.short_id) ?? [],
summary: summary, summary: summary,
@ -132,11 +129,7 @@ export const repositoriesController = new Hono()
const { id, snapshotId } = c.req.param(); const { id, snapshotId } = c.req.param();
const snapshot = await repositoriesService.getSnapshotDetails(id, snapshotId); const snapshot = await repositoriesService.getSnapshotDetails(id, snapshotId);
let duration = 0; const duration = getSnapshotDuration(snapshot.summary);
if (snapshot.summary) {
const { backup_start, backup_end } = snapshot.summary;
duration = new Date(backup_end).getTime() - new Date(backup_start).getTime();
}
const response = { const response = {
short_id: snapshot.short_id, short_id: snapshot.short_id,
@ -144,7 +137,7 @@ export const repositoriesController = new Hono()
time: new Date(snapshot.time).getTime(), time: new Date(snapshot.time).getTime(),
paths: snapshot.paths, paths: snapshot.paths,
hostname: snapshot.hostname, hostname: snapshot.hostname,
size: snapshot.summary?.total_bytes_processed || 0, size: snapshot.summary?.total_bytes_processed ?? 0,
tags: snapshot.tags ?? [], tags: snapshot.tags ?? [],
retentionCategories: [], retentionCategories: [],
summary: snapshot.summary, summary: snapshot.summary,

View file

@ -0,0 +1,4 @@
export function getSnapshotDuration(summary?: { backup_start: string; backup_end: string }): number {
if (!summary) return 0;
return new Date(summary.backup_end).getTime() - new Date(summary.backup_start).getTime();
}