diff --git a/app/client/api-client/client.gen.ts b/app/client/api-client/client.gen.ts index 301c95c0..200ef153 100644 --- a/app/client/api-client/client.gen.ts +++ b/app/client/api-client/client.gen.ts @@ -15,4 +15,4 @@ export type CreateClientConfig = ( override?: Config, ) => Config & T>; -export const client = createClient(createConfig({ baseUrl: "http://localhost:4096" })); +export const client = createClient(createConfig({ baseUrl: "http://192.168.2.42:4096" })); diff --git a/app/client/api-client/types.gen.ts b/app/client/api-client/types.gen.ts index 74fbae0e..e36f73c1 100644 --- a/app/client/api-client/types.gen.ts +++ b/app/client/api-client/types.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts export type ClientOptions = { - baseUrl: "http://localhost:4096" | (string & {}); + baseUrl: "http://192.168.2.42:4096" | (string & {}); }; export type GetStatusData = { @@ -1643,6 +1643,7 @@ export type ListSnapshotsResponses = { 200: Array<{ duration: number; paths: Array; + retentionCategories: Array; short_id: string; size: number; tags: Array; @@ -1711,6 +1712,7 @@ export type GetSnapshotDetailsResponses = { 200: { duration: number; paths: Array; + retentionCategories: Array; short_id: string; size: number; tags: Array; diff --git a/app/client/components/retention-category-badges.tsx b/app/client/components/retention-category-badges.tsx new file mode 100644 index 00000000..93869cb0 --- /dev/null +++ b/app/client/components/retention-category-badges.tsx @@ -0,0 +1,85 @@ +import { cn } from "~/client/lib/utils"; +import { HoverCard, HoverCardContent, HoverCardTrigger } from "./ui/hover-card"; + +interface RetentionCategoryBadgesProps { + categories: string[]; + className?: string; +} + +const categoryColors: Record = { + latest: "bg-blue-500/20 text-blue-700 dark:text-blue-300 border-blue-500/30", + hourly: "bg-cyan-500/20 text-cyan-700 dark:text-cyan-300 border-cyan-500/30", + daily: "bg-green-500/20 text-green-700 dark:text-green-300 border-green-500/30", + weekly: "bg-orange-500/20 text-orange-700 dark:text-orange-300 border-orange-500/30", + monthly: "bg-purple-500/20 text-purple-700 dark:text-purple-300 border-purple-500/30", + yearly: "bg-red-500/20 text-red-700 dark:text-red-300 border-red-500/30", +}; + +const categoryLabels: Record = { + latest: "Latest", + hourly: "Hourly", + daily: "Daily", + weekly: "Weekly", + monthly: "Monthly", + yearly: "Yearly", +}; + +function Badge({ category }: { category: string }) { + return ( + + {categoryLabels[category] || category} + + ); +} + +export function RetentionCategoryBadges({ categories, className }: RetentionCategoryBadgesProps) { + if (categories.length === 0) { + return null; + } + + const order = ["latest", "hourly", "daily", "weekly", "monthly", "yearly"]; + const sortedCategories = [...categories].sort((a, b) => { + const indexA = order.indexOf(a); + const indexB = order.indexOf(b); + if (indexA === -1) return 1; + if (indexB === -1) return -1; + return indexA - indexB; + }); + + const firstCategory = sortedCategories[0]; + const hasMore = sortedCategories.length > 1; + + if (!hasMore) { + return ( +
+ +
+ ); + } + + return ( + + + + + +
+ {sortedCategories.map((category) => ( + + ))} +
+
+
+ ); +} diff --git a/app/client/modules/backups/components/snapshot-timeline.tsx b/app/client/modules/backups/components/snapshot-timeline.tsx index c8200643..02092a22 100644 --- a/app/client/modules/backups/components/snapshot-timeline.tsx +++ b/app/client/modules/backups/components/snapshot-timeline.tsx @@ -4,6 +4,7 @@ import { ByteSize } from "~/client/components/bytes-size"; import { Card, CardContent } from "~/client/components/ui/card"; import { formatDateWithMonth, formatShortDate, formatTime } from "~/client/lib/datetime"; import { cn } from "~/client/lib/utils"; +import { RetentionCategoryBadges } from "~/client/components/retention-category-badges"; interface Props { snapshots: ListSnapshotsResponse; @@ -58,10 +59,9 @@ export const SnapshotTimeline = (props: Props) => {
- {snapshots.map((snapshot, index) => { + {snapshots.map((snapshot) => { const date = new Date(snapshot.time); const isSelected = snapshotId === snapshot.short_id; - const isLatest = index === snapshots.length - 1; return ( ); })} diff --git a/app/client/modules/repositories/routes/snapshot-details.tsx b/app/client/modules/repositories/routes/snapshot-details.tsx index 824905a4..cd382073 100644 --- a/app/client/modules/repositories/routes/snapshot-details.tsx +++ b/app/client/modules/repositories/routes/snapshot-details.tsx @@ -107,7 +107,7 @@ export default function SnapshotDetailsPage({ loaderData }: Route.ComponentProps fallback={ } > diff --git a/app/server/modules/repositories/repositories.controller.ts b/app/server/modules/repositories/repositories.controller.ts index 08c73088..f9a2b015 100644 --- a/app/server/modules/repositories/repositories.controller.ts +++ b/app/server/modules/repositories/repositories.controller.ts @@ -96,13 +96,11 @@ export const repositoriesController = new Hono() if (backupId) { try { const schedule = await backupsService.getScheduleByShortId(backupId); - if (schedule?.retentionPolicy) { - const snapshotsForCategories = res.map((snapshot) => ({ - short_id: snapshot.short_id, - time: new Date(snapshot.time).getTime(), - })); - retentionCategories = computeRetentionCategories(snapshotsForCategories, schedule.retentionPolicy); - } + const snapshotsForCategories = res.map((snapshot) => ({ + short_id: snapshot.short_id, + time: new Date(snapshot.time).getTime(), + })); + retentionCategories = computeRetentionCategories(snapshotsForCategories, schedule.retentionPolicy); } catch (error) { logger.warn(`Failed to fetch retention policy for backup ID ${backupId}`, toMessage(error)); } diff --git a/app/server/utils/retention-categories.ts b/app/server/utils/retention-categories.ts index 980be032..c27dd2f3 100644 --- a/app/server/utils/retention-categories.ts +++ b/app/server/utils/retention-categories.ts @@ -19,7 +19,7 @@ const RETENTION_RULES = [ export const computeRetentionCategories = (snapshots: SnapshotInfo[], policy: RetentionPolicy | null) => { const categories = new Map(); - if (!policy || snapshots.length === 0) return categories; + if (snapshots.length === 0) return categories; const sorted = [...snapshots].sort((a, b) => b.time - a.time); @@ -27,10 +27,9 @@ export const computeRetentionCategories = (snapshots: SnapshotInfo[], policy: Re const tags = categories.get(id) ?? []; if (!tags.includes(tag)) categories.set(id, [...tags, tag]); }; + addTag(sorted[0].short_id, "latest"); - if (policy.keepLast && policy.keepLast > 0) { - sorted.slice(0, 1).forEach((s) => addTag(s.short_id, "latest")); - } + if (!policy) return categories; for (const { prop, tag, fmt } of RETENTION_RULES) { const limit = policy[prop];