mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-07-31 12:56:24 +00:00
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:
@@ -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")}>
|
||||
|
||||
Reference in New Issue
Block a user