mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-06-15 04:47:00 +00:00
complete i18n migration to /[locale]/ with EN+ES content
Full rewrite of the docs site under app/[locale]/ with next-intl in localePrefix:"always" mode. Every page now exists at both /en/<path> and /es/<path>; the root / shows a meta-refresh + JS redirect to /<defaultLocale>/ so GitHub Pages serves something on the apex URL. Highlights: - 107 doc pages migrated to file-per-page JSON namespaces under messages/en/ and messages/es/. Spanish content is fully translated (no copy-of-English placeholders). - New documentation for the Active Suppressions section in the Settings tab and the per-event Dismiss dropdown in the Health Monitor modal. - New screenshots: dismiss-duration-dropdown.png and an updated health-suppression-settings.png. - Pagefind integrated for client-side search; index is built on every CI deploy (not committed). - RSS feeds: per-locale at /<locale>/rss.xml plus root /rss.xml for backward compat. - Removed the dead app/[locale]/guides/[slug]/ route — every guide now has its own static page and no markdown source remains. - Fixed orphan link /guides/nvidia -> /guides/nvidia-manual in docs/hardware/nvidia-host. - Removed obsolete components (footer2, calendar, drawer). Verified locally with `npm ci && npm run build`: 2804 files in out/, 231 pages indexed by pagefind, root redirect intact, both locale roots and the new Active Suppressions docs render OK.
This commit is contained in:
351
web/app/[locale]/docs/storage-share/host-local-disk/page.tsx
Normal file
351
web/app/[locale]/docs/storage-share/host-local-disk/page.tsx
Normal file
@@ -0,0 +1,351 @@
|
||||
import type { Metadata } from "next"
|
||||
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
|
||||
import { Link } from "@/i18n/navigation"
|
||||
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.storageShare.hostLocalDisk.meta" })
|
||||
return {
|
||||
title: t("title"),
|
||||
description: t("description"),
|
||||
openGraph: {
|
||||
title: t("ogTitle"),
|
||||
description: t("ogDescription"),
|
||||
type: "article",
|
||||
url: "https://macrimi.github.io/ProxMenux/docs/storage-share/host-local-disk",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
type CompareRow = { label: string; dir?: string; zfs?: string; dirRich?: string; zfsRich?: string }
|
||||
type StringItem = string
|
||||
type PresetRow = { preset: string; content: string; use?: string; useRich?: string }
|
||||
type RelatedItem = { href: string; label: string; tail?: string }
|
||||
|
||||
export default async function HostLocalDiskPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ locale: string }>
|
||||
}) {
|
||||
const { locale } = await params
|
||||
setRequestLocale(locale)
|
||||
const t = await getTranslations({ locale, namespace: "docs.storageShare.hostLocalDisk" })
|
||||
|
||||
const messages = (await getMessages({ locale })) as unknown as {
|
||||
docs: { storageShare: { hostLocalDisk: {
|
||||
compare: { rows: CompareRow[] }
|
||||
format: { items: StringItem[] }
|
||||
reuse: { items: StringItem[] }
|
||||
presets: { rows: PresetRow[] }
|
||||
troubleshoot: { noDisksItems: StringItem[] }
|
||||
related: { items: RelatedItem[] }
|
||||
} } }
|
||||
}
|
||||
const compareRows = messages.docs.storageShare.hostLocalDisk.compare.rows
|
||||
const formatItems = messages.docs.storageShare.hostLocalDisk.format.items
|
||||
const reuseItems = messages.docs.storageShare.hostLocalDisk.reuse.items
|
||||
const presetRows = messages.docs.storageShare.hostLocalDisk.presets.rows
|
||||
const noDisksItems = messages.docs.storageShare.hostLocalDisk.troubleshoot.noDisksItems
|
||||
const relatedItems = messages.docs.storageShare.hostLocalDisk.related.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={10}
|
||||
scriptPath="share/disk_host.sh"
|
||||
/>
|
||||
|
||||
<Callout variant="info" title={t("intro.title")}>
|
||||
{t.rich("intro.body", { em, strong })}
|
||||
</Callout>
|
||||
|
||||
<Callout variant="danger" title={t("destructive.title")}>
|
||||
{t.rich("destructive.body", { em, strong, code })}
|
||||
</Callout>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("compare.heading")}</h2>
|
||||
<div className="overflow-x-auto mb-4 rounded-md border border-gray-200">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead className="bg-gray-50 text-left text-gray-700">
|
||||
<tr>
|
||||
<th className="px-4 py-2 font-semibold"> </th>
|
||||
<th className="px-4 py-2 font-semibold">{t("compare.headerDir")}</th>
|
||||
<th className="px-4 py-2 font-semibold">{t("compare.headerZfs")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 text-gray-800">
|
||||
{compareRows.map((row, idx) => (
|
||||
<tr key={row.label}>
|
||||
<td className="px-4 py-2 font-semibold">{row.label}</td>
|
||||
<td className="px-4 py-2">{row.dirRich ? t.rich(`compare.rows.${idx}.dirRich`, { code }) : row.dir}</td>
|
||||
<td className="px-4 py-2">{row.zfsRich ? t.rich(`compare.rows.${idx}.zfsRich`, { code }) : row.zfs}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("opening.heading")}</h2>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">
|
||||
{t.rich("opening.body", { strong })}
|
||||
</p>
|
||||
|
||||
<Image
|
||||
src="/share/host-local-disk-menu.png"
|
||||
alt={t("opening.imageAlt")}
|
||||
width={900}
|
||||
height={500}
|
||||
className="rounded shadow-lg my-6"
|
||||
/>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("howRuns.heading")}</h2>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("howRuns.body")}</p>
|
||||
|
||||
<pre className="bg-gray-100 text-gray-800 p-4 rounded-md overflow-x-auto text-sm my-4 border border-gray-200 leading-snug">
|
||||
{`┌─────────────────────────────────────────────┐
|
||||
│ PHASE 1 — Detect, inspect, plan │
|
||||
│ (nothing touched yet) │
|
||||
└──────────────────┬──────────────────────────┘
|
||||
▼
|
||||
Dependency check
|
||||
└─ parted / mkfs.ext4 / mkfs.xfs / blkid /
|
||||
lsblk / sgdisk present?
|
||||
If any missing → apt-get install
|
||||
parted e2fsprogs util-linux
|
||||
xfsprogs gdisk btrfs-progs
|
||||
│
|
||||
▼
|
||||
Disk detection (lsblk -dn -e 7,11)
|
||||
│
|
||||
▼
|
||||
Safety filter
|
||||
├─ Hidden: type != "disk" (skip partitions)
|
||||
├─ Hidden: read-only (ro=1)
|
||||
├─ Hidden: /dev/zd* (ZFS volumes, not disks)
|
||||
├─ Hidden: used by host storage
|
||||
│ (root pool, mounted paths, ZFS/LVM)
|
||||
└─ Hidden: referenced by any VM/LXC config
|
||||
│
|
||||
▼
|
||||
User selects a disk
|
||||
(menu shows disk path + size + model)
|
||||
│
|
||||
▼
|
||||
Disk inspection (blkid / lsblk)
|
||||
├─ Has data → offer 2 actions:
|
||||
│ ├─ Format disk (ERASE all)
|
||||
│ └─ Use existing filesystem
|
||||
└─ Empty → only "Format disk"
|
||||
│
|
||||
▼
|
||||
If "Format" was chosen:
|
||||
Filesystem picker
|
||||
├─ ext4 → dir storage (recommended general use)
|
||||
├─ xfs → dir storage (large files / VMs)
|
||||
├─ btrfs → dir storage (snapshots / compression)
|
||||
└─ zfs → ZFS POOL storage (different path)
|
||||
│
|
||||
▼
|
||||
Storage ID (default: "disk-<device>")
|
||||
Mount path (default: "/mnt/<storage-id>")
|
||||
Content types (4 presets + custom):
|
||||
├─ 1. VM Storage → images,backup
|
||||
├─ 2. Standard NAS → backup,iso,vztmpl
|
||||
├─ 3. All types → images,backup,iso,vztmpl,snippets
|
||||
└─ 4. Custom → free CSV input
|
||||
│
|
||||
┌──────── Cancel OR Confirm ────┐
|
||||
▼ ▼
|
||||
Exit, nothing ┌─────────────────┴─────────────────┐
|
||||
was changed │ PHASE 2 — Execute │
|
||||
└─────────────────┬─────────────────┘
|
||||
▼
|
||||
FORMAT PATH (destructive):
|
||||
├─ Final "ERASE confirmation" dialog
|
||||
│ → Cancel exits here
|
||||
├─ wipefs + sgdisk --zap-all
|
||||
├─ parted/sgdisk: create partition
|
||||
├─ ZFS pre-flight:
|
||||
│ • zpool command present?
|
||||
│ • pool name not already in use?
|
||||
├─ mkfs.<fs> / zpool create
|
||||
│ (mkfs.ext4 / xfs / btrfs / zfs pool)
|
||||
├─ Non-ZFS: mount -t <fs> + UUID
|
||||
│ entry in /etc/fstab with
|
||||
│ defaults,nofail
|
||||
└─ ZFS: zpool manages its own mount
|
||||
▼
|
||||
REUSE PATH (existing fs):
|
||||
├─ blkid detects filesystem type
|
||||
├─ mkdir mount point
|
||||
├─ mount <disk> <path>
|
||||
└─ UUID entry in /etc/fstab
|
||||
▼
|
||||
Register in Proxmox:
|
||||
├─ filesystem == zfs →
|
||||
│ pvesm add zfspool <id> \\
|
||||
│ --pool <id> \\
|
||||
│ --content <csv>
|
||||
└─ otherwise →
|
||||
pvesm add dir <id> \\
|
||||
--path <mount-path> \\
|
||||
--content <csv>
|
||||
▼
|
||||
Summary + "visible in Datacenter →
|
||||
Storage" confirmation`}
|
||||
</pre>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-12 mb-4 text-gray-900">{t("format.heading")}</h2>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("format.intro")}</p>
|
||||
<ol className="list-decimal pl-6 mb-4 text-gray-800 leading-relaxed space-y-2">
|
||||
{formatItems.map((_, idx) => (
|
||||
<li key={idx}>{t.rich(`format.items.${idx}`, { code, strong })}</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
<Callout variant="tip" title={t("format.tipTitle")}>
|
||||
{t.rich("format.tipBody", { code })}
|
||||
</Callout>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("reuse.heading")}</h2>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t("reuse.intro")}</p>
|
||||
<ol className="list-decimal pl-6 mb-4 text-gray-800 leading-relaxed space-y-2">
|
||||
{reuseItems.map((_, idx) => (
|
||||
<li key={idx}>{t.rich(`reuse.items.${idx}`, { code, strong })}</li>
|
||||
))}
|
||||
</ol>
|
||||
|
||||
<Callout variant="warning" title={t("reuse.warnTitle")}>
|
||||
{t.rich("reuse.warnBody", { em, code })}
|
||||
</Callout>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("presets.heading")}</h2>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("presets.intro", { code })}</p>
|
||||
<div className="overflow-x-auto mb-4 rounded-md border border-gray-200">
|
||||
<table className="min-w-full text-sm">
|
||||
<thead className="bg-gray-50 text-left text-gray-700">
|
||||
<tr>
|
||||
<th className="px-4 py-2 font-semibold">{t("presets.headerPreset")}</th>
|
||||
<th className="px-4 py-2 font-semibold">{t("presets.headerContent")}</th>
|
||||
<th className="px-4 py-2 font-semibold">{t("presets.headerUse")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 text-gray-800">
|
||||
{presetRows.map((row, idx) => (
|
||||
<tr key={row.preset}>
|
||||
<td className="px-4 py-2 font-semibold">{row.preset}</td>
|
||||
<td className="px-4 py-2 font-mono">{row.content}</td>
|
||||
<td className="px-4 py-2">
|
||||
{row.useRich ? t.rich(`presets.rows.${idx}.useRich`, { code }) : row.use}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<Callout variant="info" title={t("presets.zfsTitle")}>
|
||||
{t.rich("presets.zfsBody", { strong, code })}
|
||||
</Callout>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("manual.heading")}</h2>
|
||||
<p className="mb-3 text-gray-800 leading-relaxed">{t("manual.extIntro")}</p>
|
||||
<CopyableCode code={`# 1. prerequisites (one-time)
|
||||
apt-get install -y parted e2fsprogs xfsprogs gdisk btrfs-progs
|
||||
|
||||
# 2. wipe + partition
|
||||
wipefs -af /dev/sdX
|
||||
sgdisk --zap-all /dev/sdX
|
||||
sgdisk -n 1:0:0 -t 1:8300 /dev/sdX
|
||||
|
||||
# 3. format
|
||||
mkfs.ext4 -L mydisk /dev/sdX1
|
||||
|
||||
# 4. mount + fstab (by UUID, nofail)
|
||||
mkdir -p /mnt/mydisk
|
||||
UUID=$(blkid -s UUID -o value /dev/sdX1)
|
||||
echo "UUID=$UUID /mnt/mydisk ext4 defaults,nofail 0 2" >> /etc/fstab
|
||||
mount /mnt/mydisk
|
||||
|
||||
# 5. register in Proxmox
|
||||
pvesm add dir mydisk \\
|
||||
--path /mnt/mydisk \\
|
||||
--content images,backup`} />
|
||||
|
||||
<p className="mb-3 mt-6 text-gray-800 leading-relaxed">{t("manual.zfsIntro")}</p>
|
||||
<CopyableCode code={`# 1. create the pool on the raw disk (no partition step needed)
|
||||
zpool create -o ashift=12 tank /dev/sdX
|
||||
|
||||
# 2. register in Proxmox
|
||||
pvesm add zfspool tank \\
|
||||
--pool tank \\
|
||||
--content images,rootdir
|
||||
|
||||
pvesm status tank`} />
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("view.heading")}</h2>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("view.body", { code })}</p>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("remove.heading")}</h2>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("remove.body", { code, strong })}</p>
|
||||
|
||||
<Callout variant="warning" title={t("remove.warnTitle")}>
|
||||
{t("remove.warnBody")}
|
||||
</Callout>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("list.heading")}</h2>
|
||||
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("list.body", { code })}</p>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-12 mb-4 text-gray-900">{t("troubleshoot.heading")}</h2>
|
||||
|
||||
<Callout variant="troubleshoot" title={t("troubleshoot.noDisksTitle")}>
|
||||
{t("troubleshoot.noDisksIntro")}
|
||||
<ul className="mt-2 list-disc list-inside space-y-1">
|
||||
{noDisksItems.map((_, idx) => (
|
||||
<li key={idx}>{t(`troubleshoot.noDisksItems.${idx}`)}</li>
|
||||
))}
|
||||
</ul>
|
||||
{t.rich("troubleshoot.noDisksOutro", { em, code })}
|
||||
</Callout>
|
||||
|
||||
<Callout variant="troubleshoot" title={t("troubleshoot.mountedTitle")}>
|
||||
{t.rich("troubleshoot.mountedBody", { code })}
|
||||
</Callout>
|
||||
|
||||
<Callout variant="troubleshoot" title={t("troubleshoot.zpoolTitle")}>
|
||||
{t.rich("troubleshoot.zpoolBody", { code })}
|
||||
</Callout>
|
||||
|
||||
<Callout variant="troubleshoot" title={t("troubleshoot.inactiveTitle")}>
|
||||
{t.rich("troubleshoot.inactiveBody", { code })}
|
||||
</Callout>
|
||||
|
||||
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("related.heading")}</h2>
|
||||
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
|
||||
{relatedItems.map((item) => (
|
||||
<li key={item.href}>
|
||||
<Link href={item.href} className="text-blue-600 hover:underline">
|
||||
{item.label}
|
||||
</Link>
|
||||
{item.tail}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user