docs: promote /web from develop for backup-restore, Log2RAM and Network Flow guides

Brings 69 files from develop under /web/:
- Full Backup & Restore section (11 pages EN + ES: overview, how-it-works, destinations,
  creating backups, scheduled jobs, restoring, cross-kernel hydration)
- Log2RAM dedicated block in post-install/optional with commands + upstream link
- Network Flow diagram documented on monitor/dashboard/network
- Rewritten category descriptions in post-install/customizable
- Fixed automated.json thresholds + link to Log2RAM section
- Updated screenshots (network-flow-overview, storage-top-row, vms modals)

No code, config or AppImage binaries touched — /web/ scope only. Merging deploys
the documentation site to the current beta release notes.
This commit is contained in:
MacRimi
2026-07-04 22:10:49 +02:00
parent a92420d654
commit 720f3fdbd2
69 changed files with 7246 additions and 48 deletions
+7 -5
View File
@@ -12,14 +12,16 @@ import CopyableCode from "@/components/CopyableCode"
// Resolve which CHANGELOG.md to read for the given locale. The canonical
// English file lives at the repo root (so GitHub displays it as-is and
// existing RSS / external consumers don't break). Localized versions
// sit under <repo>/lang/<locale>/CHANGELOG.md. Falls back to English if
// the localized file doesn't exist yet — so a partially-translated
// changelog still renders (in EN) instead of 404'ing.
// existing RSS / external consumers don't break). Localized versions sit
// under <repo>/web/data/changelog/<locale>.md — separate from the
// /lang/ directory which is now reserved for runtime translation JSON
// shipped to the host install. Falls back to English if the localized
// file doesn't exist yet — so a partially-translated changelog still
// renders (in EN) instead of 404'ing.
function resolveChangelogPath(locale: string): string {
const repoRoot = path.join(process.cwd(), "..")
if (locale && locale !== "en") {
const localized = path.join(repoRoot, "lang", locale, "CHANGELOG.md")
const localized = path.join(process.cwd(), "data", "changelog", `${locale}.md`)
if (fs.existsSync(localized)) return localized
}
return path.join(repoRoot, "CHANGELOG.md")
@@ -0,0 +1,473 @@
import type { Metadata } from "next"
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
import Image from "next/image"
import { Link } from "@/i18n/navigation"
import { DocHeader } from "@/components/ui/doc-header"
import { Callout } from "@/components/ui/callout"
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "docs.backupRestore.creatingBackups.meta" })
return {
title: t("title"),
description: t("description"),
keywords: [
"proxmox interactive backup",
"proxmenux backup menu",
"backup profile default custom",
"hb_prepare_staging",
"proxmenux backup wizard",
],
alternates: { canonical: "https://proxmenux.com/docs/backup-restore/creating-backups" },
openGraph: {
title: t("ogTitle"),
description: t("ogDescription"),
type: "article",
url: "https://proxmenux.com/docs/backup-restore/creating-backups",
},
twitter: {
card: "summary_large_image",
title: t("twitterTitle"),
description: t("twitterDescription"),
},
}
}
type EntryRow = { entry: string; path: string; detail: string }
type MatrixRow = { combo: string; destination: string; profile: string; action: string }
type StepRow = { step: string; name: string; detail: string }
type WritingRow = { topic: string; detail: string }
type WhereNextItem = { label: string; href: string; tail: string }
export default async function CreatingBackupsPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.backupRestore.creatingBackups" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { backupRestore: { creatingBackups: {
entryPoints: { rows: EntryRow[] }
matrix: { rows: MatrixRow[] }
commonPipeline: { steps: StepRow[] }
included: {
globalItems: string[]
rootItems: string[]
proxmenuxItems: string[]
notInProfileItems: string[]
}
writing: { rows: WritingRow[] }
whereNext: { items: WhereNextItem[] }
} } }
}
const cb = messages.docs.backupRestore.creatingBackups
const entryRows = cb.entryPoints.rows
const matrixRows = cb.matrix.rows
const pipelineSteps = cb.commonPipeline.steps
const globalItems = cb.included.globalItems
const rootItems = cb.included.rootItems
const proxmenuxItems = cb.included.proxmenuxItems
const notInProfileItems = cb.included.notInProfileItems
const writingRows = cb.writing.rows
const whereNextItems = cb.whereNext.items
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={9}
/>
<Callout variant="info" title={t("intro.title")}>
{t.rich("intro.body", { code, em, strong })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("entryPoints.heading")}
</h2>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Entry point</th>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Path</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Detail</th>
</tr>
</thead>
<tbody className="text-gray-800">
{entryRows.map((row, idx) => (
<tr key={row.entry} className={idx < entryRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.entry}</strong></td>
<td className="px-3 py-2 align-top font-mono text-xs whitespace-nowrap">{row.path}</td>
<td className="px-3 py-2 align-top">{t.rich(`entryPoints.rows.${idx}.detail`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("modes.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("modes.body", { code, strong })}
</p>
<p className="mb-4 text-gray-800 leading-relaxed">
<Link href="/docs/backup-restore/scheduled-jobs" className="text-blue-600 hover:underline font-medium">
{t("modes.seeAlso")}
</Link>
</p>
<figure className="my-6">
<a
href="/images/docs/backup-restore/scheduled-backup-monitor.png"
target="_blank"
rel="noopener noreferrer"
className="block cursor-zoom-in group"
aria-label={t("modes.monitorAlt")}
>
<Image
src="/images/docs/backup-restore/scheduled-backup-monitor.png"
alt={t("modes.monitorAlt")}
width={1400}
height={700}
className="rounded-lg border border-gray-200 shadow-sm w-full h-auto transition group-hover:shadow-md"
/>
</a>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("modes.monitorCaption")}
</figcaption>
</figure>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("matrix.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t("matrix.intro")}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">#</th>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Destination</th>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Profile</th>
<th className="text-left px-3 py-2 border-b border-gray-200">What it does</th>
</tr>
</thead>
<tbody className="text-gray-800">
{matrixRows.map((row, idx) => (
<tr key={row.combo} className={idx < matrixRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top font-mono text-xs whitespace-nowrap"><strong>{row.combo}</strong></td>
<td className="px-3 py-2 align-top whitespace-nowrap">{row.destination}</td>
<td className="px-3 py-2 align-top whitespace-nowrap">{row.profile}</td>
<td className="px-3 py-2 align-top">{t.rich(`matrix.rows.${idx}.action`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("profiles.heading")}
</h2>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("profiles.defaultTitle")}
</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("profiles.defaultBody", { code, em })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("profiles.customTitle")}
</h3>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("profiles.customBody", { code, em })}
</p>
<figure className="my-6">
<a
href="/images/docs/backup-restore/custom-picker.png"
target="_blank"
rel="noopener noreferrer"
className="block cursor-zoom-in group"
aria-label={t("profiles.customPickerAlt")}
>
<Image
src="/images/docs/backup-restore/custom-picker.png"
alt={t("profiles.customPickerAlt")}
width={1400}
height={700}
className="rounded-lg border border-gray-200 shadow-sm w-full h-auto transition group-hover:shadow-md"
/>
</a>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("profiles.customPickerCaption")}
</figcaption>
</figure>
<figure className="my-6">
<a
href="/images/docs/backup-restore/manage-custom-paths.png"
target="_blank"
rel="noopener noreferrer"
className="block cursor-zoom-in group"
aria-label={t("profiles.manageCustomAlt")}
>
<Image
src="/images/docs/backup-restore/manage-custom-paths.png"
alt={t("profiles.manageCustomAlt")}
width={1400}
height={700}
className="rounded-lg border border-gray-200 shadow-sm w-full h-auto transition group-hover:shadow-md"
/>
</a>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("profiles.manageCustomCaption")}
</figcaption>
</figure>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("commonPipeline.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("commonPipeline.intro", { code })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">#</th>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Step</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Detail</th>
</tr>
</thead>
<tbody className="text-gray-800">
{pipelineSteps.map((row, idx) => (
<tr key={row.step} className={idx < pipelineSteps.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top font-mono text-xs whitespace-nowrap"><strong>{row.step}</strong></td>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.name}</strong></td>
<td className="px-3 py-2 align-top">{t.rich(`commonPipeline.steps.${idx}.detail`, { code, em })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("included.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("included.intro", { code })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t.rich("included.globalTitle", { code })}
</h3>
<ul className="mb-6 space-y-1">
{globalItems.map((_, idx) => (
<li key={idx} className="text-gray-800 leading-relaxed pl-1">
<span className="text-gray-400 mr-2"></span>
{t.rich(`included.globalItems.${idx}`, { code })}
</li>
))}
</ul>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t.rich("included.rootTitle", { code })}
</h3>
<p className="mb-3 text-gray-800 leading-relaxed">
{t.rich("included.rootBody", { code })}
</p>
<ul className="mb-6 space-y-1">
{rootItems.map((_, idx) => (
<li key={idx} className="text-gray-800 leading-relaxed pl-1">
<span className="text-gray-400 mr-2"></span>
{t.rich(`included.rootItems.${idx}`, { code })}
</li>
))}
</ul>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t.rich("included.proxmenuxTitle", { code })}
</h3>
<p className="mb-3 text-gray-800 leading-relaxed">
{t.rich("included.proxmenuxBody", { code })}
</p>
<ul className="mb-6 space-y-1">
{proxmenuxItems.map((_, idx) => (
<li key={idx} className="text-gray-800 leading-relaxed pl-1">
<span className="text-gray-400 mr-2"></span>
{t.rich(`included.proxmenuxItems.${idx}`, { code })}
</li>
))}
</ul>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("included.notInProfileTitle")}
</h3>
<p className="mb-3 text-gray-800 leading-relaxed">
{t.rich("included.notInProfileBody", { code })}
</p>
<ul className="mb-6 space-y-2">
{notInProfileItems.map((_, idx) => (
<li key={idx} className="text-gray-800 leading-relaxed pl-1">
<span className="text-gray-400 mr-2"></span>
{t.rich(`included.notInProfileItems.${idx}`, { code, strong })}
</li>
))}
</ul>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("included.customPathsTitle")}
</h3>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("included.customPathsBody", { code })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("archiveStructure.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("archiveStructure.intro", { code })}
</p>
<pre className="bg-gray-50 border border-gray-200 rounded-md p-4 text-sm font-mono text-gray-800 overflow-x-auto whitespace-pre leading-relaxed mb-6">
{t("archiveStructure.tree")}
</pre>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("confirmation.heading")}
</h2>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("confirmation.body", { code })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("writing.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t("writing.intro")}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Topic</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Detail</th>
</tr>
</thead>
<tbody className="text-gray-800">
{writingRows.map((row, idx) => (
<tr key={row.topic} className={idx < writingRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.topic}</strong></td>
<td className="px-3 py-2 align-top">{t.rich(`writing.rows.${idx}.detail`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("finishedScreens.heading")}
</h2>
<p className="mb-6 text-gray-800 leading-relaxed">
{t("finishedScreens.intro")}
</p>
<figure className="my-6">
<a
href="/images/docs/backup-restore/backup-finished-scripts.png"
target="_blank"
rel="noopener noreferrer"
className="block cursor-zoom-in group"
aria-label={t("finishedScreens.scriptsAlt")}
>
<Image
src="/images/docs/backup-restore/backup-finished-scripts.png"
alt={t("finishedScreens.scriptsAlt")}
width={1400}
height={700}
className="rounded-lg border border-gray-200 shadow-sm w-full h-auto transition group-hover:shadow-md"
/>
</a>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("finishedScreens.scriptsCaption")}
</figcaption>
</figure>
<figure className="my-6">
<a
href="/images/docs/backup-restore/backup-finished-monitor.png"
target="_blank"
rel="noopener noreferrer"
className="block cursor-zoom-in group"
aria-label={t("finishedScreens.monitorAlt")}
>
<Image
src="/images/docs/backup-restore/backup-finished-monitor.png"
alt={t("finishedScreens.monitorAlt")}
width={1400}
height={700}
className="rounded-lg border border-gray-200 shadow-sm w-full h-auto transition group-hover:shadow-md"
/>
</a>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("finishedScreens.monitorCaption")}
</figcaption>
</figure>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("whereNext.heading")}
</h2>
<ul className="mb-6 space-y-2">
{whereNextItems.map((item) => (
<li key={item.href} className="text-gray-800 leading-relaxed">
<Link href={item.href} className="text-blue-600 hover:underline font-medium">
{item.label}
</Link>
<span>{item.tail}</span>
</li>
))}
</ul>
</div>
)
}
@@ -0,0 +1,293 @@
import type { Metadata } from "next"
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
import { AlertTriangle } from "lucide-react"
import { Link } from "@/i18n/navigation"
import { DocHeader } from "@/components/ui/doc-header"
import { Callout } from "@/components/ui/callout"
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "docs.backupRestore.crossKernel.meta" })
return {
title: t("title"),
description: t("description"),
keywords: [
"proxmox cross-kernel restore",
"bk_older bk_newer safe subset",
"IOMMU VFIO hydration",
"kernel-agnostic restore",
"hb_unsafe_paths_cross_version",
"HB_HYDRATION_APPLIED",
],
alternates: { canonical: "https://proxmenux.com/docs/backup-restore/cross-kernel" },
openGraph: {
title: t("ogTitle"),
description: t("ogDescription"),
type: "article",
url: "https://proxmenux.com/docs/backup-restore/cross-kernel",
},
twitter: {
card: "summary_large_image",
title: t("twitterTitle"),
description: t("twitterDescription"),
},
}
}
type DirectionRow = { direction: string; condition: string; behavior: string }
type CategoryRow = { category: string; paths: string; reason: string }
type PhaseRow = { phase: string; detail: string }
type ScenarioRow = { scenario: string; detail: string }
type CodeRefRow = { component: string; location: string }
type WhereNextItem = { label: string; href: string; tail: string }
export default async function CrossKernelPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.backupRestore.crossKernel" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { backupRestore: { crossKernel: {
directionCheck: { rows: DirectionRow[] }
safeSubsetFilter: { categoryRows: CategoryRow[] }
hydration: { phaseRows: PhaseRow[] }
concreteExamples: { rows: ScenarioRow[] }
codeReference: { rows: CodeRefRow[] }
whereNext: { items: WhereNextItem[] }
} } }
}
const ck = messages.docs.backupRestore.crossKernel
const directionRows = ck.directionCheck.rows
const categoryRows = ck.safeSubsetFilter.categoryRows
const phaseRows = ck.hydration.phaseRows
const scenarioRows = ck.concreteExamples.rows
const codeRefRows = ck.codeReference.rows
const whereNextItems = ck.whereNext.items
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={16}
/>
<Callout variant="info" title={t("intro.title")}>
{t.rich("intro.body", { code, strong, em })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("directionCheck.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("directionCheck.intro", { code })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Direction</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Condition</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Behaviour</th>
</tr>
</thead>
<tbody className="text-gray-800">
{directionRows.map((row, idx) => (
<tr key={row.direction} className={idx < directionRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top font-mono text-xs whitespace-nowrap"><strong>{row.direction}</strong></td>
<td className="px-3 py-2 align-top">{t.rich(`directionCheck.rows.${idx}.condition`, { code })}</td>
<td className="px-3 py-2 align-top">{t.rich(`directionCheck.rows.${idx}.behavior`, { code, em })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("whyBkNewerIsSafe.heading")}
</h2>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("whyBkNewerIsSafe.body", { code, em })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("safeSubsetFilter.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("safeSubsetFilter.intro", { code })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Category</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Paths</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Why they are skipped</th>
</tr>
</thead>
<tbody className="text-gray-800">
{categoryRows.map((row, idx) => (
<tr key={row.category} className={idx < categoryRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.category}</strong></td>
<td className="px-3 py-2 align-top">{t.rich(`safeSubsetFilter.categoryRows.${idx}.paths`, { code })}</td>
<td className="px-3 py-2 align-top">{t.rich(`safeSubsetFilter.categoryRows.${idx}.reason`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("safeSubsetFilter.outroBody", { code })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("hydration.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("hydration.intro", { code })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Phase</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Detail</th>
</tr>
</thead>
<tbody className="text-gray-800">
{phaseRows.map((row, idx) => (
<tr key={row.phase} className={idx < phaseRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.phase}</strong></td>
<td className="px-3 py-2 align-top">{t.rich(`hydration.phaseRows.${idx}.detail`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("planCommit.heading")}
</h2>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("planCommit.body", { code, em })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("flowDiagram.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t("flowDiagram.intro")}
</p>
<pre className="bg-gray-50 border border-gray-200 rounded-md p-4 text-xs sm:text-sm font-mono text-gray-800 overflow-x-auto whitespace-pre leading-relaxed mb-6">
{t("flowDiagram.diagram")}
</pre>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("concreteExamples.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t("concreteExamples.intro")}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Scenario</th>
<th className="text-left px-3 py-2 border-b border-gray-200">What hydration does</th>
</tr>
</thead>
<tbody className="text-gray-800">
{scenarioRows.map((row, idx) => (
<tr key={row.scenario} className={idx < scenarioRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.scenario}</strong></td>
<td className="px-3 py-2 align-top">{t.rich(`concreteExamples.rows.${idx}.detail`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="my-8 rounded-lg border border-amber-200 bg-amber-50 p-5">
<div className="flex items-start gap-3">
<AlertTriangle className="h-5 w-5 text-amber-600 shrink-0 mt-0.5" aria-hidden="true" />
<div>
<h3 className="text-base font-semibold text-amber-900 mb-1">
{t("callout.warningTitle")}
</h3>
<p className="text-sm text-amber-900/90 leading-relaxed">
{t.rich("callout.warningBody", { code })}
</p>
</div>
</div>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("codeReference.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t("codeReference.intro")}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Component</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Location</th>
</tr>
</thead>
<tbody className="text-gray-800">
{codeRefRows.map((row, idx) => (
<tr key={row.component} className={idx < codeRefRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.component}</strong></td>
<td className="px-3 py-2 align-top">{t.rich(`codeReference.rows.${idx}.location`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("whereNext.heading")}
</h2>
<ul className="mb-6 space-y-2">
{whereNextItems.map((item) => (
<li key={item.href} className="text-gray-800 leading-relaxed">
<Link href={item.href} className="text-blue-600 hover:underline font-medium">
{item.label}
</Link>
<span>{item.tail}</span>
</li>
))}
</ul>
</div>
)
}
@@ -0,0 +1,396 @@
import type { Metadata } from "next"
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
import { ExternalLink } from "lucide-react"
import { DocHeader } from "@/components/ui/doc-header"
import { Callout } from "@/components/ui/callout"
import CopyableCode from "@/components/CopyableCode"
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "docs.backupRestore.destinations.borg.meta" })
return {
title: t("title"),
description: t("description"),
keywords: [
"borg backup proxmox",
"borg repository",
"borg ssh",
"borg repokey",
"borg serve restrict-to-path",
"borg-linux64",
"borgbackup deduplication",
],
alternates: { canonical: "https://proxmenux.com/docs/backup-restore/destinations/borg" },
openGraph: {
title: t("ogTitle"),
description: t("ogDescription"),
type: "article",
url: "https://proxmenux.com/docs/backup-restore/destinations/borg",
},
twitter: {
card: "summary_large_image",
title: t("twitterTitle"),
description: t("twitterDescription"),
},
}
}
type PriorityRow = { priority: string; source: string; detail: string }
type RepoTypeRow = { type: string; url: string; detail: string }
type StrategyRow = { mode: string; label: string; detail: string }
type FileRow = { file: string; content: string }
type EnvRow = { var: string; value: string; purpose: string }
type ReferenceItem = { label: string; href: string; tail: string }
type RequirementRow = { requirement: string; detail: string }
export default async function BorgDestinationPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.backupRestore.destinations.borg" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { backupRestore: { destinations: { borg: {
binarySourcing: { rows: PriorityRow[] }
repoTypes: { rows: RepoTypeRow[] }
serverSetup: { hostChoicesItems: string[]; requirementsRows: RequirementRow[] }
sshAuth: { strategyRows: StrategyRow[] }
savedTargets: { rows: FileRow[] }
runtimeEnv: { rows: EnvRow[] }
references: { items: ReferenceItem[] }
} } } }
}
const borg = messages.docs.backupRestore.destinations.borg
const binaryRows = borg.binarySourcing.rows
const repoTypeRows = borg.repoTypes.rows
const hostChoicesItems = borg.serverSetup.hostChoicesItems
const requirementsRows = borg.serverSetup.requirementsRows
const strategyRows = borg.sshAuth.strategyRows
const savedTargetRows = borg.savedTargets.rows
const envRows = borg.runtimeEnv.rows
const references = borg.references.items
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={13}
/>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("aboutBorg.heading")}
</h2>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("aboutBorg.body", { code })}
</p>
<Callout variant="info" title={t("intro.title")}>
{t.rich("intro.body", { code, em })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("binarySourcing.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("binarySourcing.intro", { code })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">#</th>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Source</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Detail</th>
</tr>
</thead>
<tbody className="text-gray-800">
{binaryRows.map((row, idx) => (
<tr key={row.priority} className={idx < binaryRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top font-mono text-xs whitespace-nowrap"><strong>{row.priority}</strong></td>
<td className="px-3 py-2 align-top whitespace-nowrap">{t.rich(`binarySourcing.rows.${idx}.source`, { code })}</td>
<td className="px-3 py-2 align-top">{t.rich(`binarySourcing.rows.${idx}.detail`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("repoTypes.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t("repoTypes.intro")}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Type</th>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Repository URL</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Detail</th>
</tr>
</thead>
<tbody className="text-gray-800">
{repoTypeRows.map((row, idx) => (
<tr key={row.type} className={idx < repoTypeRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top font-mono text-xs whitespace-nowrap"><strong>{row.type}</strong></td>
<td className="px-3 py-2 align-top font-mono text-xs whitespace-nowrap">{row.url}</td>
<td className="px-3 py-2 align-top">{t.rich(`repoTypes.rows.${idx}.detail`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("serverSetup.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("serverSetup.intro", { code, em })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("serverSetup.hostChoicesTitle")}
</h3>
<p className="mb-3 text-gray-800 leading-relaxed">
{t("serverSetup.hostChoicesBody")}
</p>
<ul className="mb-4 space-y-2">
{hostChoicesItems.map((_, idx) => (
<li key={idx} className="text-gray-800 leading-relaxed pl-1">
<span className="text-gray-400 mr-2"></span>
{t.rich(`serverSetup.hostChoicesItems.${idx}`, { code, em })}
</li>
))}
</ul>
<Callout variant="warning" title={t("serverSetup.lxcWarningTitle")}>
{t("serverSetup.lxcWarningBody")}
</Callout>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("serverSetup.requirementsTitle")}
</h3>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Requirement</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Detail</th>
</tr>
</thead>
<tbody className="text-gray-800">
{requirementsRows.map((row, idx) => (
<tr key={idx} className={idx < requirementsRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top">{t.rich(`serverSetup.requirementsRows.${idx}.requirement`, { code })}</td>
<td className="px-3 py-2 align-top">{t.rich(`serverSetup.requirementsRows.${idx}.detail`, { code, em })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("serverSetup.minimalSetupTitle")}
</h3>
<p className="mb-3 text-gray-800 leading-relaxed">
{t("serverSetup.minimalSetupBody")}
</p>
<CopyableCode code={t("serverSetup.minimalSetupCmd")} language="bash" />
<p className="mt-4 mb-6 text-gray-800 leading-relaxed">
{t.rich("serverSetup.minimalSetupNote", { code })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("sshAuth.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("sshAuth.intro", { code, strong })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("sshAuth.strategiesTitle")}
</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t("sshAuth.strategiesIntro")}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Mode</th>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Best for</th>
<th className="text-left px-3 py-2 border-b border-gray-200">What it does</th>
</tr>
</thead>
<tbody className="text-gray-800">
{strategyRows.map((row, idx) => (
<tr key={row.mode} className={idx < strategyRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top font-mono text-xs whitespace-nowrap"><strong>{row.mode}</strong></td>
<td className="px-3 py-2 align-top whitespace-nowrap">{row.label}</td>
<td className="px-3 py-2 align-top">{t.rich(`sshAuth.strategyRows.${idx}.detail`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("sshAuth.restrictTitle")}
</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("sshAuth.restrictBody", { code })}
</p>
<CopyableCode code={t("sshAuth.restrictLine")} language="text" />
<p className="mt-4 mb-6 text-gray-800 leading-relaxed">
{t.rich("sshAuth.restrictNote", { code })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("savedTargets.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t("savedTargets.intro")}
</p>
<div className="overflow-x-auto mb-4">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">File</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Content</th>
</tr>
</thead>
<tbody className="text-gray-800">
{savedTargetRows.map((row, idx) => (
<tr key={row.file} className={idx < savedTargetRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top font-mono text-xs whitespace-nowrap"><strong>{row.file}</strong></td>
<td className="px-3 py-2 align-top">{t.rich(`savedTargets.rows.${idx}.content`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mb-6 text-gray-800 leading-relaxed">
{t("savedTargets.outro")}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("encryption.heading")}
</h2>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("encryption.body", { code })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("runtimeEnv.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("runtimeEnv.intro", { code })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Variable</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Value</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Purpose</th>
</tr>
</thead>
<tbody className="text-gray-800">
{envRows.map((row, idx) => (
<tr key={row.var} className={idx < envRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top font-mono text-xs whitespace-nowrap"><strong>{row.var}</strong></td>
<td className="px-3 py-2 align-top font-mono text-xs">{row.value}</td>
<td className="px-3 py-2 align-top">{t.rich(`runtimeEnv.rows.${idx}.purpose`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("archiveFormat.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t("archiveFormat.intro")}
</p>
<CopyableCode code={t("archiveFormat.namePattern")} language="text" />
<p className="mt-4 mb-6 text-gray-800 leading-relaxed">
{t.rich("archiveFormat.retentionBody", { code })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("restoreAccess.heading")}
</h2>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("restoreAccess.body", { code, em })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("references.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t("references.intro")}
</p>
<ul className="mb-6 space-y-2">
{references.map((item) => (
<li key={item.href} className="text-gray-800 leading-relaxed">
<a
href={item.href}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline font-medium inline-flex items-center gap-1"
>
{item.label}
<ExternalLink className="h-3 w-3" aria-hidden="true" />
</a>
<span>{item.tail}</span>
</li>
))}
</ul>
</div>
)
}
@@ -0,0 +1,196 @@
import type { Metadata } from "next"
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
import { DocHeader } from "@/components/ui/doc-header"
import { Callout } from "@/components/ui/callout"
import CopyableCode from "@/components/CopyableCode"
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "docs.backupRestore.destinations.local.meta" })
return {
title: t("title"),
description: t("description"),
keywords: [
"proxmox local backup",
"tar.zst archive",
"proxmox backup usb",
"proxmenux local destination",
"proxmox backup to usb drive",
"vzdump directory",
],
alternates: { canonical: "https://proxmenux.com/docs/backup-restore/destinations/local" },
openGraph: {
title: t("ogTitle"),
description: t("ogDescription"),
type: "article",
url: "https://proxmenux.com/docs/backup-restore/destinations/local",
},
twitter: {
card: "summary_large_image",
title: t("twitterTitle"),
description: t("twitterDescription"),
},
}
}
type OptionItem = string
type StateRow = { state: string; shown: string; action: string }
export default async function LocalDestinationPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.backupRestore.destinations.local" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { backupRestore: { destinations: { local: {
targetConfig: { options: OptionItem[] }
usbFlow: { stateRows: StateRow[] }
} } } }
}
const loc = messages.docs.backupRestore.destinations.local
const options = loc.targetConfig.options
const stateRows = loc.usbFlow.stateRows
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={7}
/>
<Callout variant="info" title={t("intro.title")}>
{t.rich("intro.body", { code, strong })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("targetConfig.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("targetConfig.intro", { code, strong, em })}
</p>
<ul className="mb-6 space-y-3">
{options.map((_, idx) => (
<li key={idx} className="text-gray-800 leading-relaxed pl-1">
<span className="text-gray-400 mr-2"></span>
{t.rich(`targetConfig.options.${idx}`, { code, strong })}
</li>
))}
</ul>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("usbFlow.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("usbFlow.intro", { code })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("usbFlow.statesTitle")}
</h3>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">State</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Menu entry</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Action</th>
</tr>
</thead>
<tbody className="text-gray-800">
{stateRows.map((row, idx) => (
<tr key={row.state} className={idx < stateRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top font-mono text-xs whitespace-nowrap"><strong>{row.state}</strong></td>
<td className="px-3 py-2 align-top font-mono text-xs">{row.shown}</td>
<td className="px-3 py-2 align-top">{t.rich(`usbFlow.stateRows.${idx}.action`, { code, strong })}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="text-sm text-gray-700 mb-6 leading-relaxed">
{t.rich("usbFlow.notMountedFallback", { code, em })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("safetyCheck.heading")}
</h2>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("safetyCheck.body", { code, strong })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("archiveFormat.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("archiveFormat.intro", { code, strong })}
</p>
<CopyableCode code={t("archiveFormat.namePattern")} language="text" />
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("archiveFormat.compressionTitle")}
</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("archiveFormat.compressionBody", { code, strong })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("archiveFormat.sourceTitle")}
</h3>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("archiveFormat.sourceBody", { code, strong })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("sidecar.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("sidecar.intro", { code, strong })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("sidecar.contentTitle")}
</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("sidecar.contentBody", { code })}
</p>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("sidecar.whyBody", { code, strong })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("restoreAccess.heading")}
</h2>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("restoreAccess.body", { code, strong })}
</p>
</div>
)
}
@@ -0,0 +1,180 @@
import type { Metadata } from "next"
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
import { Link } from "@/i18n/navigation"
import { DocHeader } from "@/components/ui/doc-header"
import { Callout } from "@/components/ui/callout"
import CopyableCode from "@/components/CopyableCode"
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "docs.backupRestore.destinations.meta" })
return {
title: t("title"),
description: t("description"),
keywords: [
"proxmox backup destinations",
"proxmox local backup",
"proxmox backup server",
"borg backup proxmox",
"tar zst backup",
"pbs snapshot",
"borg repository",
],
alternates: { canonical: "https://proxmenux.com/docs/backup-restore/destinations" },
openGraph: {
title: t("ogTitle"),
description: t("ogDescription"),
type: "article",
url: "https://proxmenux.com/docs/backup-restore/destinations",
},
twitter: {
card: "summary_large_image",
title: t("twitterTitle"),
description: t("twitterDescription"),
},
}
}
type ComparisonRow = {
feature: string
local: string
pbs: string
borg: string
}
type WhereNextItem = { label: string; href: string; tail: string }
export default async function DestinationsIndexPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.backupRestore.destinations" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { backupRestore: { destinations: {
comparison: { rows: ComparisonRow[] }
whereNext: { items: WhereNextItem[] }
} } }
}
const dest = messages.docs.backupRestore.destinations
const rows = dest.comparison.rows
const whereNextItems = dest.whereNext.items
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={9}
/>
<Callout variant="info" title={t("intro.title")}>
{t.rich("intro.body", { code, strong })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("comparison.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("comparison.intro", { strong })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">
{t("comparison.captionCode")}
</th>
<th className="text-left px-3 py-2 border-b border-gray-200">
{t("comparison.captionLocal")}
</th>
<th className="text-left px-3 py-2 border-b border-gray-200">
{t("comparison.captionPbs")}
</th>
<th className="text-left px-3 py-2 border-b border-gray-200">
{t("comparison.captionBorg")}
</th>
</tr>
</thead>
<tbody className="text-gray-800">
{rows.map((row, idx) => (
<tr key={row.feature} className={idx < rows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.feature}</strong></td>
<td className="px-3 py-2 align-top">{t.rich(`comparison.rows.${idx}.local`, { code })}</td>
<td className="px-3 py-2 align-top">{t.rich(`comparison.rows.${idx}.pbs`, { code })}</td>
<td className="px-3 py-2 align-top">{t.rich(`comparison.rows.${idx}.borg`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("sameArchive.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("sameArchive.body", { code, em })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("extractStandalone.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("extractStandalone.intro", { code, strong })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("comparison.captionLocal")}
</h3>
<CopyableCode code={t("extractStandalone.localCmd")} language="bash" />
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("comparison.captionPbs")}
</h3>
<CopyableCode code={t("extractStandalone.pbsCmd")} language="bash" />
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("comparison.captionBorg")}
</h3>
<CopyableCode code={t("extractStandalone.borgCmd")} language="bash" />
<p className="mt-4 mb-6 text-sm text-gray-600 italic">
{t.rich("extractStandalone.note", { em })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("whereNext.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("whereNext.intro", { em })}
</p>
<ul className="mb-6 space-y-2">
{whereNextItems.map((item) => (
<li key={item.href} className="text-gray-800 leading-relaxed">
<Link href={item.href} className="text-blue-600 hover:underline font-medium">
{item.label}
</Link>
<span>{item.tail}</span>
</li>
))}
</ul>
</div>
)
}
@@ -0,0 +1,262 @@
import type { Metadata } from "next"
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
import { Star, ExternalLink } from "lucide-react"
import Image from "next/image"
import { DocHeader } from "@/components/ui/doc-header"
import { Callout } from "@/components/ui/callout"
import CopyableCode from "@/components/CopyableCode"
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "docs.backupRestore.destinations.pbs.meta" })
return {
title: t("title"),
description: t("description"),
keywords: [
"proxmox backup server",
"pbs encryption keyfile",
"pbs recovery passphrase",
"proxmox-backup-client",
"pxar snapshot",
"pbs chunk deduplication",
"proxmenux pbs",
],
alternates: { canonical: "https://proxmenux.com/docs/backup-restore/destinations/pbs" },
openGraph: {
title: t("ogTitle"),
description: t("ogDescription"),
type: "article",
url: "https://proxmenux.com/docs/backup-restore/destinations/pbs",
},
twitter: {
card: "summary_large_image",
title: t("twitterTitle"),
description: t("twitterDescription"),
},
}
}
type SourceRow = { source: string; path: string; content: string }
type ReferenceItem = { label: string; href: string; tail: string }
export default async function PbsDestinationPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.backupRestore.destinations.pbs" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { backupRestore: { destinations: { pbs: {
repoSelection: { sourceRows: SourceRow[] }
references: { items: ReferenceItem[] }
} } } }
}
const sourceRows = messages.docs.backupRestore.destinations.pbs.repoSelection.sourceRows
const references = messages.docs.backupRestore.destinations.pbs.references.items
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={11}
/>
<div className="mb-6 -mt-3">
<span className="inline-flex items-center gap-1.5 rounded-full border border-emerald-200 bg-emerald-50 px-2.5 py-0.5 text-xs font-semibold uppercase tracking-wide text-emerald-800">
<Star className="h-3.5 w-3.5 fill-current" aria-hidden="true" />
{t("recommendedBadge")}
</span>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("aboutPbs.heading")}
</h2>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("aboutPbs.body", { code })}
</p>
<Callout variant="info" title={t("intro.title")}>
{t.rich("intro.body", { code, em })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("repoSelection.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("repoSelection.intro", { code })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Source</th>
<th className="text-left px-3 py-2 border-b border-gray-200">On-disk state</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Content</th>
</tr>
</thead>
<tbody className="text-gray-800">
{sourceRows.map((row, idx) => (
<tr key={row.source} className={idx < sourceRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.source}</strong></td>
<td className="px-3 py-2 align-top font-mono text-xs">{row.path}</td>
<td className="px-3 py-2 align-top">{t.rich(`repoSelection.sourceRows.${idx}.content`, { code, em })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("repoSelection.menuTitle")}
</h3>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("repoSelection.menuBody", { code })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("backupCommand.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("backupCommand.intro", { code })}
</p>
<CopyableCode code={t("backupCommand.cmd")} language="bash" />
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("backupCommand.backupIdTitle")}
</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("backupCommand.backupIdBody", { code, em })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("backupCommand.pxarTitle")}
</h3>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("backupCommand.pxarBody", { code })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("encryption.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("encryption.intro", { code, strong })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("encryption.keyfileTitle")}
</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("encryption.keyfileBody", { code })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("encryption.recoveryTitle")}
</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("encryption.recoveryBody", { code })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("encryption.blobUploadTitle")}
</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("encryption.blobUploadBody1", { code })}
</p>
<figure className="my-6">
<a
href="/images/docs/backup-restore/pbs-paired-backup-groups.png"
target="_blank"
rel="noopener noreferrer"
className="block cursor-zoom-in group"
aria-label={t("encryption.blobUploadImageAlt")}
>
<Image
src="/images/docs/backup-restore/pbs-paired-backup-groups.png"
alt={t("encryption.blobUploadImageAlt")}
width={1400}
height={700}
className="rounded-lg border border-gray-200 shadow-sm w-full h-auto transition group-hover:shadow-md"
/>
</a>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("encryption.blobUploadImageCaption")}
</figcaption>
</figure>
<h4 className="text-base font-semibold mt-6 mb-2 text-gray-900">
{t("encryption.blobUploadConstraintTitle")}
</h4>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("encryption.blobUploadConstraintBody", { code, strong, em })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("encryption.recoverTitle")}
</h3>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("encryption.recoverBody", { code })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("restoreAccess.heading")}
</h2>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("restoreAccess.body", { code })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("references.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t("references.intro")}
</p>
<ul className="mb-6 space-y-2">
{references.map((item) => (
<li key={item.href} className="text-gray-800 leading-relaxed">
<a
href={item.href}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline font-medium inline-flex items-center gap-1"
>
{item.label}
<ExternalLink className="h-3 w-3" aria-hidden="true" />
</a>
<span>{item.tail}</span>
</li>
))}
</ul>
</div>
)
}
@@ -0,0 +1,310 @@
import type { Metadata } from "next"
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
import { DocHeader } from "@/components/ui/doc-header"
import { Callout } from "@/components/ui/callout"
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "docs.backupRestore.howItWorks.meta" })
return {
title: t("title"),
description: t("description"),
keywords: [
"proxmox backup internals",
"proxmox manifest json",
"proxmox backup collectors",
"packages.manual.list",
"components_status.json",
"proxmenux rootfs",
"proxmox rsync backup",
"apply_cluster_postboot",
],
alternates: { canonical: "https://proxmenux.com/docs/backup-restore/how-it-works" },
openGraph: {
title: t("ogTitle"),
description: t("ogDescription"),
type: "article",
url: "https://proxmenux.com/docs/backup-restore/how-it-works",
},
twitter: {
card: "summary_large_image",
title: t("twitterTitle"),
description: t("twitterDescription"),
},
}
}
type CategoryRow = { category: string; paths: string; why: string }
type CollectorRow = { collector: string; produces: string; content: string }
type InstallerRow = { component: string; installer: string; action: string }
type StageRow = { stage: string; name: string; reads: string; action: string }
export default async function HowItWorksPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.backupRestore.howItWorks" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { backupRestore: { howItWorks: {
rootfs: { categoryRows: CategoryRow[] }
manifest: { collectorRows: CollectorRow[] }
applications: { installerRows: InstallerRow[] }
restoreFlow: { stageRows: StageRow[] }
} } }
}
const hw = messages.docs.backupRestore.howItWorks
const categoryRows = hw.rootfs.categoryRows
const collectorRows = hw.manifest.collectorRows
const installerRows = hw.applications.installerRows
const stageRows = hw.restoreFlow.stageRows
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={14}
/>
<Callout variant="info" title={t("intro.title")}>
{t.rich("intro.body", { code, strong })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("layout.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("layout.intro", { code, strong })}
</p>
<figure className="my-6">
<pre className="bg-gray-50 border border-gray-200 rounded-md p-4 text-sm font-mono text-gray-800 overflow-x-auto whitespace-pre leading-relaxed">
{t("layout.tree")}
</pre>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("layout.treeCaption")}
</figcaption>
</figure>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("rootfs.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("rootfs.intro", { code, strong, em })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("rootfs.defaultProfileTitle")}
</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("rootfs.defaultProfileBody", { code, strong })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">{t("rootfs.categoriesTitle")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Paths</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Why</th>
</tr>
</thead>
<tbody className="text-gray-800">
{categoryRows.map((row, idx) => (
<tr key={row.category} className={idx < categoryRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.category}</strong></td>
<td className="px-3 py-2 align-top font-mono text-xs text-gray-700 leading-relaxed">{row.paths}</td>
<td className="px-3 py-2 align-top">{t.rich(`rootfs.categoryRows.${idx}.why`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("rootfs.customTitle")}
</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("rootfs.customBody", { code, em, strong })}
</p>
<h4 className="text-base font-semibold mt-5 mb-2 text-gray-900">
{t("rootfs.customExtrasTitle")}
</h4>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("rootfs.customExtrasBody", { code, em, strong })}
</p>
<h4 className="text-base font-semibold mt-5 mb-2 text-gray-900">
{t("rootfs.customModeTitle")}
</h4>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("rootfs.customModeBody", { code, em, strong })}
</p>
<h4 className="text-base font-semibold mt-5 mb-2 text-gray-900">
{t("rootfs.customMissingTitle")}
</h4>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("rootfs.customMissingBody", { code })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("manifest.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("manifest.intro", { code, strong })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Collector</th>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Produces</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Content</th>
</tr>
</thead>
<tbody className="text-gray-800">
{collectorRows.map((row, idx) => (
<tr key={row.collector} className={idx < collectorRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top font-mono text-xs whitespace-nowrap"><strong>{row.collector}</strong></td>
<td className="px-3 py-2 align-top font-mono text-xs whitespace-nowrap">{row.produces}</td>
<td className="px-3 py-2 align-top">{t.rich(`manifest.collectorRows.${idx}.content`, { code, em })}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="text-sm text-gray-500 italic text-center mb-6">
{t("manifest.orchestratorCaption")}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("manifest.schemaTitle")}
</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("manifest.schemaBody", { code })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("applications.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("applications.intro", { code, strong })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("applications.packagesTitle")}
</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("applications.packagesBody", { code, em })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("applications.componentsTitle")}
</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("applications.componentsBody", { code, em })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">
{t("applications.componentInstallersTitle")}
</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("applications.componentInstallersBody", { code })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Component</th>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Installer</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Action on <code>--auto-reinstall</code></th>
</tr>
</thead>
<tbody className="text-gray-800">
{installerRows.map((row, idx) => (
<tr key={row.component} className={idx < installerRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top font-mono text-xs whitespace-nowrap"><strong>{row.component}</strong></td>
<td className="px-3 py-2 align-top font-mono text-xs whitespace-nowrap">{row.installer}</td>
<td className="px-3 py-2 align-top">{t.rich(`applications.installerRows.${idx}.action`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("restoreFlow.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("restoreFlow.intro", { code, strong })}
</p>
<div className="overflow-x-auto mb-2">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">#</th>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Stage</th>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Reads</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Action</th>
</tr>
</thead>
<tbody className="text-gray-800">
{stageRows.map((row, idx) => (
<tr key={row.stage} className={idx < stageRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap font-mono text-xs"><strong>{row.stage}</strong></td>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.name}</strong></td>
<td className="px-3 py-2 align-top font-mono text-xs">{row.reads}</td>
<td className="px-3 py-2 align-top">{t.rich(`restoreFlow.stageRows.${idx}.action`, { code, em })}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="text-sm text-gray-500 italic text-center mb-6">
{t("restoreFlow.stagesCaption")}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("whyItWorks.heading")}
</h2>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("whyItWorks.body", { code, strong })}
</p>
</div>
)
}
@@ -0,0 +1,162 @@
import type { Metadata } from "next"
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
import { Link } from "@/i18n/navigation"
import { DocHeader } from "@/components/ui/doc-header"
import { Callout } from "@/components/ui/callout"
import { DataFlowDiagram } from "@/components/ui/data-flow-diagram"
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "docs.backupRestore.meta" })
return {
title: t("title"),
description: t("description"),
keywords: [
"proxmox backup",
"proxmox host backup",
"proxmox restore",
"proxmox backup server",
"borg backup proxmox",
"cross-kernel restore",
"proxmox host restore",
"vzdump alternative",
"proxmenux backup",
"proxmox migration",
],
alternates: { canonical: "https://proxmenux.com/docs/backup-restore" },
openGraph: {
title: t("ogTitle"),
description: t("ogDescription"),
type: "article",
url: "https://proxmenux.com/docs/backup-restore",
},
twitter: {
card: "summary_large_image",
title: t("twitterTitle"),
description: t("twitterDescription"),
},
}
}
type WhatItIsNotItem = string
type WhereNextItem = { label: string; href: string; tail: string }
export default async function BackupRestoreOverviewPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.backupRestore" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { backupRestore: {
whatItIsNot: { items: WhatItIsNotItem[] }
whereNext: { items: WhereNextItem[] }
} }
}
const br = messages.docs.backupRestore
const whatItIsNotItems = br.whatItIsNot.items
const whereNextItems = br.whereNext.items
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={8}
/>
<Callout variant="info" title={t("intro.title")}>
{t.rich("intro.body", { code, strong })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("whatItIsNot.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("whatItIsNot.intro", { strong })}
</p>
<ul className="mb-6 space-y-3">
{whatItIsNotItems.map((_, idx) => (
<li key={idx} className="text-gray-800 leading-relaxed pl-1">
<span className="text-gray-400 mr-2"></span>
{t.rich(`whatItIsNot.items.${idx}`, { code, strong })}
</li>
))}
</ul>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("threePillars.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("threePillars.intro", { strong })}
</p>
<DataFlowDiagram
nodes={[
{ variant: "source", label: t("threePillars.pillar1Label"), detail: t("threePillars.pillar1Detail") },
{ variant: "source", label: t("threePillars.pillar2Label"), detail: t("threePillars.pillar2Detail") },
{ variant: "source", label: t("threePillars.pillar3Label"), detail: t("threePillars.pillar3Detail") },
]}
caption={t("threePillars.diagramCaption")}
/>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("restoreIsUniversal.heading")}
</h2>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("restoreIsUniversal.body", { strong })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("twoInterfaces.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("twoInterfaces.intro", { strong })}
</p>
<DataFlowDiagram
nodes={[
{ variant: "bridge", label: t("twoInterfaces.cliLabel"), detail: t("twoInterfaces.cliDetail") },
{ variant: "bridge", label: t("twoInterfaces.webLabel"), detail: t("twoInterfaces.webDetail") },
]}
bidirectional
/>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("whereNext.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("whereNext.intro", { em })}
</p>
<ul className="mb-6 space-y-2">
{whereNextItems.map((item) => (
<li key={item.href} className="text-gray-800 leading-relaxed">
<Link href={item.href} className="text-blue-600 hover:underline font-medium">
{item.label}
</Link>
<span>{item.tail}</span>
</li>
))}
</ul>
</div>
)
}
@@ -0,0 +1,404 @@
import type { Metadata } from "next"
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
import Image from "next/image"
import { Link } from "@/i18n/navigation"
import { DocHeader } from "@/components/ui/doc-header"
import { Callout } from "@/components/ui/callout"
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "docs.backupRestore.restoring.meta" })
return {
title: t("title"),
description: t("description"),
keywords: [
"proxmox restore",
"proxmenux restore",
"hb_compat_check",
"apply_pending_restore",
"apply_cluster_postboot",
"path classification hot reboot dangerous",
"destructive rollback",
],
alternates: { canonical: "https://proxmenux.com/docs/backup-restore/restoring" },
openGraph: {
title: t("ogTitle"),
description: t("ogDescription"),
type: "article",
url: "https://proxmenux.com/docs/backup-restore/restoring",
},
twitter: {
card: "summary_large_image",
title: t("twitterTitle"),
description: t("twitterDescription"),
},
}
}
type ActionRow = { action: string; detail: string }
type OutputRow = { output: string; detail: string }
type ModeRow = { mode: string; detail: string }
type ClassRow = { class: string; detail: string }
type FileRow = { file: string; content: string }
type TaskRow = { task: string; detail: string }
type TimeRow = { stage: string; time: string; detail: string }
type LogRow = { log: string; detail: string }
type WhereNextItem = { label: string; href: string; tail: string }
export default async function RestoringPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.backupRestore.restoring" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { backupRestore: { restoring: {
threeActions: { actionRows: ActionRow[] }
compatibilityCheck: { outputRows: OutputRow[] }
twoModes: { modeRows: ModeRow[] }
pathClassification: { rows: ClassRow[] }
pendingMachinery: { rows: FileRow[] }
postbootDispatcher: { tasks: TaskRow[] }
tenMinutes: { rows: TimeRow[] }
logs: { rows: LogRow[] }
whereNext: { items: WhereNextItem[] }
} } }
}
const rs = messages.docs.backupRestore.restoring
const actionRows = rs.threeActions.actionRows
const outputRows = rs.compatibilityCheck.outputRows
const modeRows = rs.twoModes.modeRows
const classRows = rs.pathClassification.rows
const pendingRows = rs.pendingMachinery.rows
const postbootTasks = rs.postbootDispatcher.tasks
const timeRows = rs.tenMinutes.rows
const logRows = rs.logs.rows
const whereNextItems = rs.whereNext.items
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
const asciiDiagram = (text: string, caption: string) => (
<figure className="my-6">
<pre className="bg-gray-50 border border-gray-200 rounded-md p-4 text-xs sm:text-sm font-mono text-gray-800 overflow-x-auto whitespace-pre leading-relaxed">
{text}
</pre>
{caption && (
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{caption}
</figcaption>
)}
</figure>
)
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={22}
/>
<Callout variant="info" title={t("intro.title")}>
{t.rich("intro.body", { code, em })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("threeActions.heading")}
</h2>
<p className="mb-6 text-gray-800 leading-relaxed">
{t("threeActions.intro")}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Action</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Detail</th>
</tr>
</thead>
<tbody className="text-gray-800">
{actionRows.map((row, idx) => (
<tr key={row.action} className={idx < actionRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.action}</strong></td>
<td className="px-3 py-2 align-top">{t.rich(`threeActions.actionRows.${idx}.detail`, { code, em })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("compatibilityCheck.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("compatibilityCheck.intro", { code })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Output</th>
<th className="text-left px-3 py-2 border-b border-gray-200">What it drives</th>
</tr>
</thead>
<tbody className="text-gray-800">
{outputRows.map((row, idx) => (
<tr key={row.output} className={idx < outputRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.output}</strong></td>
<td className="px-3 py-2 align-top">{t.rich(`compatibilityCheck.outputRows.${idx}.detail`, { code, em })}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("compatibilityCheck.reportBody", { code })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("twoModes.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("twoModes.intro", { em })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Mode</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Detail</th>
</tr>
</thead>
<tbody className="text-gray-800">
{modeRows.map((row, idx) => (
<tr key={row.mode} className={idx < modeRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.mode}</strong></td>
<td className="px-3 py-2 align-top">{t.rich(`twoModes.modeRows.${idx}.detail`, { code, em })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("pathClassification.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("pathClassification.intro", { code })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Class</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Behaviour</th>
</tr>
</thead>
<tbody className="text-gray-800">
{classRows.map((row, idx) => (
<tr key={row.class} className={idx < classRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap font-mono text-xs"><strong>{row.class}</strong></td>
<td className="px-3 py-2 align-top">{t.rich(`pathClassification.rows.${idx}.detail`, { code, em, strong })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("fullFlow.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t("fullFlow.intro")}
</p>
{asciiDiagram(t("fullFlow.diagram"), "")}
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("pendingMachinery.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("pendingMachinery.intro", { code })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">File</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Content</th>
</tr>
</thead>
<tbody className="text-gray-800">
{pendingRows.map((row, idx) => (
<tr key={row.file} className={idx < pendingRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap font-mono text-xs"><strong>{row.file}</strong></td>
<td className="px-3 py-2 align-top">{t.rich(`pendingMachinery.rows.${idx}.content`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("pendingMachinery.unitBody", { code })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("postbootDispatcher.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("postbootDispatcher.intro", { code })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Task</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Detail</th>
</tr>
</thead>
<tbody className="text-gray-800">
{postbootTasks.map((row, idx) => (
<tr key={row.task} className={idx < postbootTasks.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.task}</strong></td>
<td className="px-3 py-2 align-top">{t.rich(`postbootDispatcher.tasks.${idx}.detail`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("postbootExample.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("postbootExample.body", { code })}
</p>
<figure className="my-6">
<a
href="/images/docs/backup-restore/postboot-completion-console.png"
target="_blank"
rel="noopener noreferrer"
className="block cursor-zoom-in group"
aria-label={t("postbootExample.imageAlt")}
>
<Image
src="/images/docs/backup-restore/postboot-completion-console.png"
alt={t("postbootExample.imageAlt")}
width={1600}
height={800}
className="rounded-lg border border-gray-200 shadow-sm w-full h-auto transition group-hover:shadow-md"
/>
</a>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("postbootExample.imageCaption")}
</figcaption>
</figure>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("tenMinutes.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t("tenMinutes.intro")}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Stage</th>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Time</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Detail</th>
</tr>
</thead>
<tbody className="text-gray-800">
{timeRows.map((row, idx) => (
<tr key={row.stage} className={idx < timeRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top"><strong>{row.stage}</strong></td>
<td className="px-3 py-2 align-top whitespace-nowrap font-mono text-xs">{row.time}</td>
<td className="px-3 py-2 align-top">{t.rich(`tenMinutes.rows.${idx}.detail`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("tenMinutes.outroBody", { code })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("destructiveRollback.heading")}
</h2>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("destructiveRollback.body", { code, em })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("logs.heading")}
</h2>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Log path</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Content</th>
</tr>
</thead>
<tbody className="text-gray-800">
{logRows.map((row, idx) => (
<tr key={row.log} className={idx < logRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top font-mono text-xs">{row.log}</td>
<td className="px-3 py-2 align-top">{t.rich(`logs.rows.${idx}.detail`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("whereNext.heading")}
</h2>
<ul className="mb-6 space-y-2">
{whereNextItems.map((item) => (
<li key={item.href} className="text-gray-800 leading-relaxed">
<Link href={item.href} className="text-blue-600 hover:underline font-medium">
{item.label}
</Link>
<span>{item.tail}</span>
</li>
))}
</ul>
</div>
)
}
@@ -0,0 +1,272 @@
import type { Metadata } from "next"
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
import { Info } from "lucide-react"
import { Link } from "@/i18n/navigation"
import { DocHeader } from "@/components/ui/doc-header"
import { Callout } from "@/components/ui/callout"
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "docs.backupRestore.scheduledJobs.meta" })
return {
title: t("title"),
description: t("description"),
keywords: [
"proxmox scheduled backup",
"proxmenux backup job",
"systemd timer backup",
"vzdump attach hook",
"keep-last keep-daily",
"backup retention proxmox",
],
alternates: { canonical: "https://proxmenux.com/docs/backup-restore/scheduled-jobs" },
openGraph: {
title: t("ogTitle"),
description: t("ogDescription"),
type: "article",
url: "https://proxmenux.com/docs/backup-restore/scheduled-jobs",
},
twitter: {
card: "summary_large_image",
title: t("twitterTitle"),
description: t("twitterDescription"),
},
}
}
type ModeRow = { mode: string; backends: string; schedule: string; retention: string }
type StepRow = { step: string; detail: string }
type LayoutRow = { path: string; content: string }
type MgmtRow = { action: string; detail: string }
type WhereNextItem = { label: string; href: string; tail: string }
export default async function ScheduledJobsPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.backupRestore.scheduledJobs" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { backupRestore: { scheduledJobs: {
intro: { modelsList: string[] }
modes: { rows: ModeRow[] }
attachDetail: { steps: StepRow[] }
storageLayout: { rows: LayoutRow[] }
runner: { steps: string[] }
management: { rows: MgmtRow[] }
whereNext: { items: WhereNextItem[] }
} } }
}
const sj = messages.docs.backupRestore.scheduledJobs
const modelsList = sj.intro.modelsList
const modeRows = sj.modes.rows
const attachSteps = sj.attachDetail.steps
const layoutRows = sj.storageLayout.rows
const runnerSteps = sj.runner.steps
const mgmtRows = sj.management.rows
const whereNextItems = sj.whereNext.items
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={11}
/>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("intro.title")}
</h2>
<p className="mb-3 text-gray-800 leading-relaxed">
{t("intro.body")}
</p>
<ul className="mb-6 space-y-3">
{modelsList.map((_, idx) => (
<li key={idx} className="text-gray-800 leading-relaxed pl-1">
<span className="text-gray-400 mr-2"></span>
{t.rich(`intro.modelsList.${idx}`, { code, strong })}
</li>
))}
</ul>
<div className="my-8 rounded-lg border border-blue-200 bg-blue-50 p-5">
<div className="flex items-start gap-3">
<Info className="h-5 w-5 text-blue-600 shrink-0 mt-0.5" aria-hidden="true" />
<div>
<h3 className="text-base font-semibold text-blue-900 mb-1">
{t("attachBadge.title")}
</h3>
<p className="text-sm text-blue-900/90 leading-relaxed">
{t.rich("attachBadge.body", { code, strong })}
</p>
</div>
</div>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("modes.heading")}
</h2>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Mode</th>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Backends</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Schedule</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Retention</th>
</tr>
</thead>
<tbody className="text-gray-800">
{modeRows.map((row, idx) => (
<tr key={row.mode} className={idx < modeRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.mode}</strong></td>
<td className="px-3 py-2 align-top whitespace-nowrap text-xs">{row.backends}</td>
<td className="px-3 py-2 align-top">{t.rich(`modes.rows.${idx}.schedule`, { code })}</td>
<td className="px-3 py-2 align-top">{t.rich(`modes.rows.${idx}.retention`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("attachDetail.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("attachDetail.intro", { code })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">#</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Detail</th>
</tr>
</thead>
<tbody className="text-gray-800">
{attachSteps.map((row, idx) => (
<tr key={row.step} className={idx < attachSteps.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top font-mono text-xs whitespace-nowrap"><strong>{row.step}</strong></td>
<td className="px-3 py-2 align-top">{t.rich(`attachDetail.steps.${idx}.detail`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("attachDetail.outroBody", { code })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("storageLayout.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t("storageLayout.intro")}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Path</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Content</th>
</tr>
</thead>
<tbody className="text-gray-800">
{layoutRows.map((row, idx) => (
<tr key={row.path} className={idx < layoutRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top font-mono text-xs">{row.path}</td>
<td className="px-3 py-2 align-top">{t.rich(`storageLayout.rows.${idx}.content`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t.rich("runner.heading", { code })}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t("runner.intro")}
</p>
<ol className="mb-6 space-y-3 list-decimal pl-6">
{runnerSteps.map((_, idx) => (
<li key={idx} className="text-gray-800 leading-relaxed">
{t.rich(`runner.steps.${idx}`, { code, strong })}
</li>
))}
</ol>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("management.heading")}
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t("management.intro")}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap">Action</th>
<th className="text-left px-3 py-2 border-b border-gray-200">Detail</th>
</tr>
</thead>
<tbody className="text-gray-800">
{mgmtRows.map((row, idx) => (
<tr key={row.action} className={idx < mgmtRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.action}</strong></td>
<td className="px-3 py-2 align-top">{t.rich(`management.rows.${idx}.detail`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("notifications.heading")}
</h2>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("notifications.body", { code })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("whereNext.heading")}
</h2>
<ul className="mb-6 space-y-2">
{whereNextItems.map((item) => (
<li key={item.href} className="text-gray-800 leading-relaxed">
<Link href={item.href} className="text-blue-600 hover:underline font-medium">
{item.label}
</Link>
<span>{item.tail}</span>
</li>
))}
</ul>
</div>
)
}
@@ -34,6 +34,7 @@ export default async function NetworkTabPage({
const messages = (await getMessages({ locale })) as unknown as {
docs: { monitor: { dashboard: { network: {
topRow: { rows: TopRow[] }
flow: { elements: string[]; pulses: string[] }
groups: { badges: string[] }
drillIn: { rows: DrillRow[] }
latency: {
@@ -48,6 +49,8 @@ export default async function NetworkTabPage({
}
const net = messages.docs.monitor.dashboard.network
const topRows = net.topRow.rows
const flowElements = net.flow.elements
const flowPulses = net.flow.pulses
const badges = net.groups.badges
const drillRows = net.drillIn.rows
const targets = net.latency.targets
@@ -96,6 +99,42 @@ export default async function NetworkTabPage({
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("flow.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("flow.intro", { strong })}
</p>
<figure className="my-4">
<img
src="/monitor/network-flow-overview.png"
alt={t("flow.imageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("flow.imageCaption")}
</figcaption>
</figure>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("flow.elementsTitle")}</h3>
<p className="mb-2 text-gray-800 leading-relaxed">{t("flow.elementsIntro")}</p>
<ul className="list-disc pl-6 mb-6 text-gray-800 leading-relaxed space-y-1">
{flowElements.map((_, idx) => (
<li key={idx}>{t.rich(`flow.elements.${idx}`, { strong })}</li>
))}
</ul>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("flow.pulsesTitle")}</h3>
<p className="mb-2 text-gray-800 leading-relaxed">{t("flow.pulsesBody")}</p>
<ul className="list-disc pl-6 mb-6 text-gray-800 leading-relaxed space-y-1">
{flowPulses.map((_, idx) => (
<li key={idx}>{t.rich(`flow.pulses.${idx}`, { strong, em })}</li>
))}
</ul>
<Callout variant="tip" title={t("flow.useTitle")}>
{t.rich("flow.useBody", { em })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("groups.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("groups.intro", { strong })}
@@ -31,6 +31,7 @@ export default async function SystemOverviewTabPage({
const messages = (await getMessages({ locale })) as unknown as {
docs: { monitor: { dashboard: { systemOverview: {
topRow: { rows: TopRow[]; thresholdsItems: string[] }
processes: { listItems: string[]; detailItems: string[] }
bottom: { storageItems: string[] }
refresh: { items: string[] }
dataCollected: { rows: DataRow[] }
@@ -40,6 +41,8 @@ export default async function SystemOverviewTabPage({
const so = messages.docs.monitor.dashboard.systemOverview
const topRows = so.topRow.rows
const thresholdsItems = so.topRow.thresholdsItems
const processListItems = so.processes.listItems
const processDetailItems = so.processes.detailItems
const storageItems = so.bottom.storageItems
const refreshItems = so.refresh.items
const dataRows = so.dataCollected.rows
@@ -135,6 +138,53 @@ export default async function SystemOverviewTabPage({
{t("topRow.sparklineBody")}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("processes.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("processes.intro", { code })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("processes.listTitle")}</h3>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{processListItems.map((_, idx) => (
<li key={idx}>{t.rich(`processes.listItems.${idx}`, { strong })}</li>
))}
</ul>
<figure className="my-6">
<img
src="/monitor/system-overview-top-processes.png"
alt={t("processes.captureListAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("processes.captureListCaption")}
</figcaption>
</figure>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("processes.detailTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("processes.detailIntro", { code })}
</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{processDetailItems.map((_, idx) => (
<li key={idx}>{t.rich(`processes.detailItems.${idx}`, { strong, code })}</li>
))}
</ul>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("processes.detailRefresh", { em, code })}
</p>
<figure className="my-6">
<img
src="/monitor/system-overview-process-detail.png"
alt={t("processes.captureDetailAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("processes.captureDetailCaption")}
</figcaption>
</figure>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("middle.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("middle.body1", { code, em })}
@@ -79,6 +79,9 @@ export default async function AutomatedPage({
const uninstallLink = (chunks: React.ReactNode) => (
<Link href="/docs/post-install/uninstall" className="text-blue-600 hover:underline">{chunks}</Link>
)
const log2ramLink = (chunks: React.ReactNode) => (
<Link href="/docs/post-install/optional#log2ram" className="text-blue-600 hover:underline">{chunks}</Link>
)
return (
<div>
@@ -116,7 +119,9 @@ export default async function AutomatedPage({
<tr key={i}>
<td className="px-4 py-2 text-gray-500 font-mono">{i + 1}</td>
<td className="px-4 py-2 font-semibold">{o.tool}</td>
<td className="px-4 py-2 text-gray-700 leading-relaxed">{o.what}</td>
<td className="px-4 py-2 text-gray-700 leading-relaxed">
{t.rich(`optimizations.${i}.what`, { link: log2ramLink })}
</td>
<td className="px-4 py-2">
<Link
href={`/docs/post-install/${o.categorySlug}`}
@@ -1,6 +1,6 @@
import type { Metadata } from "next"
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
import { Plus } from "lucide-react"
import { Plus, ExternalLink } from "lucide-react"
import CopyableCode from "@/components/CopyableCode"
export async function generateMetadata({
@@ -63,6 +63,7 @@ export default async function OptionalSettingsPage({
testing: { doesItems: string[] }
fastfetch: { doesItems: string[]; customItems: string[]; logos: Logo[] }
figurine: { doesItems: string[] }
log2ram: { doesItems: string[] }
} } }
}
const cephItems = messages.docs.postInstall.optional.ceph.doesItems
@@ -73,6 +74,7 @@ export default async function OptionalSettingsPage({
const fastfetchCustomItems = messages.docs.postInstall.optional.fastfetch.customItems
const fastfetchLogos = messages.docs.postInstall.optional.fastfetch.logos
const figurineItems = messages.docs.postInstall.optional.figurine.doesItems
const log2ramItems = messages.docs.postInstall.optional.log2ram.doesItems
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
@@ -328,6 +330,46 @@ chmod +x "/etc/profile.d/figurine.sh"
<p className="mt-4">{t("figurine.outro")}</p>
<h3 className="text-xl font-semibold mt-16 mb-4 flex items-center">
<StepNumber number={7} />
{t("log2ram.title")}
</h3>
<p className="mb-4">
{t.rich("log2ram.intro", { code, em })}
</p>
<p className="mb-4 flex items-center gap-1 text-sm">
<strong className="mr-1">{t("log2ram.upstreamLabel")}</strong>
<a
href={t("log2ram.upstreamUrl")}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline inline-flex items-center gap-1"
>
{t("log2ram.upstreamLinkLabel")}
<ExternalLink className="h-3.5 w-3.5" aria-hidden="true" />
</a>
</p>
<p className="mb-4">
<strong>{t("log2ram.doesLabel")}</strong>
</p>
<ul className="list-disc pl-5 mb-4">
{log2ramItems.map((_, idx) => (
<li key={idx}>{t.rich(`log2ram.doesItems.${idx}`, { code, em })}</li>
))}
</ul>
<p className="mb-2">
<strong>{t("log2ram.howUseLabel")}</strong> {t("log2ram.howUseBody")}
</p>
<p className="text-lg mt-6 mb-2">
<strong>{t("log2ram.verifyLabel")}</strong>
</p>
<CopyableCode code={t.raw("log2ram.verifyCode") as string} />
<section className="mt-12 p-4 bg-blue-100 rounded-md">
<h2 className="text-xl font-semibold mb-2">{t("autoApplication.title")}</h2>
<p>{t("autoApplication.body")}</p>
@@ -64,7 +64,15 @@ export default async function PostInstallStoragePage({
/>
<Callout variant="info" title={t("intro.title")}>
{t.rich("intro.body", { strong })}
{t.rich("intro.body", {
strong,
code,
link: (chunks) => (
<Link href="/docs/post-install/optional#log2ram" className="text-blue-700 hover:underline">
{chunks}
</Link>
),
})}
</Callout>
<Callout variant="warning" title={t("notTrackedTitle")}>