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:
MacRimi
2026-05-31 12:41:10 +02:00
parent 875910b4d7
commit 5ca3463bf6
649 changed files with 83958 additions and 11096 deletions

View File

@@ -0,0 +1,265 @@
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.hostIscsi.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-iscsi",
},
}
}
type VocabRow = { term: string; meaningRich: string }
type StringItem = string
type RelatedItem = { href: string; label: string; tail?: string }
export default async function HostIscsiPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.storageShare.hostIscsi" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { storageShare: { hostIscsi: {
vocab: { rows: VocabRow[] }
add: { items: StringItem[] }
troubleshoot: { discoverItems: StringItem[] }
related: { items: RelatedItem[] }
} } }
}
const vocabRows = messages.docs.storageShare.hostIscsi.vocab.rows
const addItems = messages.docs.storageShare.hostIscsi.add.items
const discoverItems = messages.docs.storageShare.hostIscsi.troubleshoot.discoverItems
const relatedItems = messages.docs.storageShare.hostIscsi.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/iscsi_host.sh"
/>
<Callout variant="info" title={t("intro.title")}>
{t.rich("intro.body", { code })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("vocab.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">{t("vocab.headerTerm")}</th>
<th className="px-4 py-2 font-semibold">{t("vocab.headerMeaning")}</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-800">
{vocabRows.map((row, idx) => (
<tr key={row.term}>
<td className="px-4 py-2 font-mono">{row.term}</td>
<td className="px-4 py-2">{t.rich(`vocab.rows.${idx}.meaningRich`, { code, em })}</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-iscsi-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 — Prepare initiator, discover │
│ (nothing touched yet in storage.cfg) │
└──────────────────┬──────────────────────────┘
Dependency check
└─ iscsiadm present? (open-iscsi package)
If missing → apt-get install open-iscsi
+ systemctl enable --now iscsid
Portal entry (manual only)
└─ user types <ip> or <ip>:<port>
If no ':' → ProxMenux appends ":3260"
Reachability validation
├─ ping -c 1 -W 3 <host> ── fail → abort
└─ nc -z -w 3 <host> <port> ── warn but continue
(iSCSI over alternative ports may block nc)
Target discovery
iscsiadm --mode discovery --type sendtargets \\
--portal <ip:port>
Extracts IQNs from stdout (lines matching ^iqn\\.)
Target selection
├─ 1 target found → auto-selected
└─ 2+ targets → menu
Storage ID
(default derived from last ':' segment of the IQN:
"iscsi-<suffix-up-to-20-chars>")
Content type (fixed — not a checklist)
└─ images iSCSI exposes block devices, so
only 'images' makes sense. No
backup/iso/vztmpl/rootdir/snippets.
┌──────── Cancel OR Confirm ────┐
▼ ▼
Exit, nothing ┌─────────────────┴─────────────────┐
was changed │ PHASE 2 — Register in Proxmox │
└─────────────────┬─────────────────┘
If storage ID already exists:
└─ ask "remove and recreate?"
└─ yes → pvesm remove <id>
└─ no → abort
pvesm add iscsi <id> \\
--portal <ip:port> \\
--target <iqn> \\
--content images
iscsid opens a persistent session to
the target; LUNs appear in /dev/disk/
by-path/ip-<ip>:<port>-iscsi-<iqn>-lun-N
Proxmox auto-connects on every boot
via the node.startup=automatic flag
written by pvesm`}
</pre>
<h2 className="text-2xl font-semibold mt-12 mb-4 text-gray-900">{t("add.heading")}</h2>
<ol className="list-decimal pl-6 mb-4 text-gray-800 leading-relaxed space-y-2">
{addItems.map((_, idx) => (
<li key={idx}>{t.rich(`add.items.${idx}`, { strong, code })}</li>
))}
</ol>
<Callout variant="warning" title={t("add.authTitle")}>
{t("add.authBody1")}
<pre className="mt-2 p-2 rounded bg-white/50 text-xs overflow-x-auto"><code>cat /etc/iscsi/initiatorname.iscsi</code></pre>
{t.rich("add.authBody2", { em, 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.body")}</p>
<CopyableCode code={`apt-get install -y open-iscsi
systemctl enable --now iscsid
# 1. discover targets on a portal
iscsiadm --mode discovery --type sendtargets \\
--portal 10.0.0.60:3260
# 2. register it in Proxmox
pvesm add iscsi myiscsi \\
--portal 10.0.0.60:3260 \\
--target iqn.2024-08.com.truenas:proxmox-pool \\
--content images
# 3. verify + see the block devices
pvesm status myiscsi
ls -la /dev/disk/by-path/ | grep iscsi`} />
<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("test.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("test.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.portalTitle")}>
{t.rich("troubleshoot.portalBody", { em })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.discoverTitle")}>
{t("troubleshoot.discoverIntro")}
<ul className="mt-2 list-disc list-inside space-y-1">
{discoverItems.map((_, idx) => (
<li key={idx}>{t.rich(`troubleshoot.discoverItems.${idx}`, { strong })}</li>
))}
</ul>
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.noTargetTitle")}>
{t("troubleshoot.noTargetBody")}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.noLunTitle")}>
{t.rich("troubleshoot.noLunBody", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.chapTitle")}>
{t.rich("troubleshoot.chapBody", { 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>
)
}

View 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">&nbsp;</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>
)
}

View File

@@ -0,0 +1,261 @@
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.storageShare.hostLocalShared.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-shared",
},
}
}
type StringItem = string
type BitsRow = { bit: string; effect: string; why: string }
type RelatedItem = { href: string; label: string; tail?: string; tailRich?: string }
export default async function HostLocalSharedPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.storageShare.hostLocalShared" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { storageShare: { hostLocalShared: {
why: { items: StringItem[] }
bits: { rows: BitsRow[] }
related: { items: RelatedItem[] }
} } }
}
const whyItems = messages.docs.storageShare.hostLocalShared.why.items
const bitsRows = messages.docs.storageShare.hostLocalShared.bits.rows
const relatedItems = messages.docs.storageShare.hostLocalShared.related.items
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const kbd = (chunks: React.ReactNode) => <kbd>{chunks}</kbd>
const mountLink = (chunks: React.ReactNode) => (
<Link href="/docs/storage-share/lxc-mount-points" className="text-blue-700 hover:underline">{chunks}</Link>
)
const diskLink = (chunks: React.ReactNode) => (
<Link href="/docs/storage-share/host-local-disk" className="text-blue-600 hover:underline">{chunks}</Link>
)
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={3}
scriptPath="share/local-shared-manager.sh"
/>
<Callout variant="info" title={t("intro.title")}>
{t.rich("intro.body", { strong, code, mountLink })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("why.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("why.intro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{whyItems.map((_, idx) => (
<li key={idx}>{t.rich(`why.items.${idx}`, { strong })}</li>
))}
</ul>
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("why.outro", { strong })}</p>
<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.rich("howRuns.body", { strong })}</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 — Pick the target path │
│ (nothing touched yet) │
└──────────────────┬──────────────────────────┘
Location picker (4 options)
├─ 1. Create new folder in /mnt
│ ProxMenux suggests a free name
│ ("shared", "shared2", "shared3"…)
├─ 2. Enter custom path
│ Any absolute path on the host
├─ 3. View existing folders in /mnt
│ Read-only summary (perms, owner,
│ free space) then back to menu
└─ 4. Cancel
Path validation
└─ Must start with "/" (absolute path)
Non-absolute → reject, re-ask
Existing directory?
└─ If /mnt/<name> already exists, ask
"Continue with permission setup?"
(adjusting existing dir is allowed)
┌──────── Cancel OR Confirm ────┐
▼ ▼
Exit, nothing ┌─────────────────┴─────────────────┐
was changed │ PHASE 2 — Create + set perms │
└─────────────────┬─────────────────┘
mkdir -p <target>
chown root:root <target>
chmod 1777 <target>
(sticky bit + world-rwx)
chmod -R a+rwX <target>
(existing content stays accessible;
X = execute only on directories)
find <target> -type d \\
-exec chmod 1777 {} +
(propagate sticky bit to subdirs)
setfacl -b -R <target>
(remove any restrictive ACLs)
setfacl -R -m u::rwx,g::rwx,o::rwx,m::rwx
(explicit rwx for user/group/other/mask)
setfacl -R -m d:u::rwx,d:g::rwx,...
(default ACLs so NEW files inherit rwx)
Register in ProxMenux share map
(pmx_share_map_set <dir> "open")
Summary:
• directory path
• permissions: 1777 (rwxrwxrwt)
• owner: root:root
• ACL: open rwx + default inheritance
• profile: works with priv and
unprivileged LXCs`}
</pre>
<h2 className="text-2xl font-semibold mt-12 mb-4 text-gray-900">{t("bits.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("bits.intro", { strong, 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("bits.headerBit")}</th>
<th className="px-4 py-2 font-semibold">{t("bits.headerEffect")}</th>
<th className="px-4 py-2 font-semibold">{t("bits.headerWhy")}</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-800">
{bitsRows.map((row) => (
<tr key={row.bit}>
<td className="px-4 py-2 font-mono">{row.bit}</td>
<td className="px-4 py-2">{row.effect}</td>
<td className="px-4 py-2">{row.why}</td>
</tr>
))}
</tbody>
</table>
</div>
<Callout variant="info" title={t("bits.privTitle")}>
{t.rich("bits.privBody", { code })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("where.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("where.intro")}</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 my-6">
<div className="rounded-lg border border-gray-200 bg-white p-4">
<h3 className="text-base font-semibold text-gray-900 mb-2">{t("where.opt1Title")}</h3>
<p className="text-sm text-gray-700 leading-relaxed">{t.rich("where.opt1Body", { code })}</p>
</div>
<div className="rounded-lg border border-gray-200 bg-white p-4">
<h3 className="text-base font-semibold text-gray-900 mb-2">{t("where.opt2Title")}</h3>
<p className="text-sm text-gray-700 leading-relaxed">{t.rich("where.opt2Body", { code })}</p>
</div>
<div className="rounded-lg border border-gray-200 bg-white p-4">
<h3 className="text-base font-semibold text-gray-900 mb-2">{t("where.opt3Title")}</h3>
<p className="text-sm text-gray-700 leading-relaxed">{t.rich("where.opt3Body", { code })}</p>
</div>
<div className="rounded-lg border border-gray-200 bg-white p-4">
<h3 className="text-base font-semibold text-gray-900 mb-2">{t("where.opt4Title")}</h3>
<p className="text-sm text-gray-700 leading-relaxed">{t.rich("where.opt4Body", { kbd })}</p>
</div>
</div>
<Callout variant="tip" title={t("where.tipTitle")}>
{t.rich("where.tipBody", { code, diskLink })}
</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.body")}</p>
<CopyableCode code={`# 1. create the directory
mkdir -p /mnt/shared
# 2. apply 1777 (sticky + world-rwx) + open existing content
chown root:root /mnt/shared
chmod 1777 /mnt/shared
chmod -R a+rwX /mnt/shared
find /mnt/shared -type d -exec chmod 1777 {} +
# 3. ACLs: explicit rwx + default inheritance for new files
setfacl -b -R /mnt/shared
setfacl -R -m u::rwx,g::rwx,o::rwx,m::rwx /mnt/shared
setfacl -R -m d:u::rwx,d:g::rwx,d:o::rwx,d:m::rwx /mnt/shared`} />
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("next.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("next.body", { strong, code, mountLink })}
</p>
<h2 className="text-2xl font-semibold mt-12 mb-4 text-gray-900">{t("troubleshoot.heading")}</h2>
<Callout variant="troubleshoot" title={t("troubleshoot.mkdirTitle")}>
{t.rich("troubleshoot.mkdirBody", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.writeTitle")}>
{t.rich("troubleshoot.writeBody", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.aclTitle")}>
{t.rich("troubleshoot.aclBody", { 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, idx) => (
<li key={item.href}>
<Link href={item.href} className="text-blue-600 hover:underline">
{item.label}
</Link>
{item.tailRich ? t.rich(`related.items.${idx}.tailRich`, { code }) : item.tail}
</li>
))}
</ul>
</div>
)
}

View File

@@ -0,0 +1,332 @@
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.hostNfs.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-nfs",
},
}
}
type StringItem = string
type ModesRow = { method: string; mount?: string; mountRich?: string; ui: string; useCase?: string; useCaseRich?: string }
type ContentRow = { type: string; allows?: string; allowsRich?: string }
type RelatedItem = { href: string; label: string; tail?: string; tailRich?: string }
export default async function HostNfsPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.storageShare.hostNfs" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { storageShare: { hostNfs: {
modes: { rows: ModesRow[] }
pvesmBranch: { items: StringItem[]; rows: ContentRow[] }
fstabBranch: { items: StringItem[]; applies: StringItem[] }
related: { items: RelatedItem[] }
} } }
}
const modesRows = messages.docs.storageShare.hostNfs.modes.rows
const pvesmItems = messages.docs.storageShare.hostNfs.pvesmBranch.items
const contentRows = messages.docs.storageShare.hostNfs.pvesmBranch.rows
const fstabItems = messages.docs.storageShare.hostNfs.fstabBranch.items
const fstabAppliesItems = messages.docs.storageShare.hostNfs.fstabBranch.applies
const relatedItems = messages.docs.storageShare.hostNfs.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>
const mountLink = (chunks: React.ReactNode) => (
<Link href="/docs/storage-share/lxc-mount-points" className="text-blue-700 hover:underline">{chunks}</Link>
)
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={6}
scriptPath="share/nfs_host.sh"
/>
<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("opening.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("opening.body", { strong })}
</p>
<Image
src="/share/host-nfs-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.rich("howRuns.body", { code })}
</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 — Discover, validate, choose │
│ (nothing touched yet) │
└──────────────────┬──────────────────────────┘
Dependency check
└─ nfs-common present? (showmount)
If missing → apt-get install nfs-common
Server selection
├─ Auto-discover (nmap -p 2049 on /24)
└─ Manual (type IP or hostname)
Reachability + showmount validation
Export selection
╔═════════════════════════════════════╗
║ MOUNT METHOD PICKER (checklist) ║
║ [ ] As Proxmox storage (pvesm) ║
║ [ ] As host fstab mount only ║
║ (mark one or both — re-prompts ║
║ if you press OK without marks) ║
╚════════════════╤════════════════════╝
┌──────────────┴──────────────┐
▼ ▼
pvesm branch fstab branch
├─ storage ID ├─ mount path
├─ content types └─ mount options
▼ ▼
┌─────────────────────────────────────────┐
│ PHASE 2 — Apply (only marked methods) │
└──────────────────┬──────────────────────┘
pvesm add nfs <id> ... + mkdir -p <path>
(auto-mount at mount -t nfs ...
/mnt/pve/<id>) append /etc/fstab
systemctl daemon-reload
chmod 1777 + setfacl
(best-effort, NFS server-side)
Summary printed`}
</pre>
<h2 className="text-2xl font-semibold mt-12 mb-4 text-gray-900">{t("modes.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("modes.intro")}</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("modes.headerMethod")}</th>
<th className="px-4 py-2 font-semibold">{t("modes.headerMount")}</th>
<th className="px-4 py-2 font-semibold">{t("modes.headerUi")}</th>
<th className="px-4 py-2 font-semibold">{t("modes.headerUseCase")}</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-800 align-top">
{modesRows.map((row, idx) => (
<tr key={idx}>
<td className="px-4 py-2">{t.rich(`modes.rows.${idx}.method`, { strong })}</td>
<td className="px-4 py-2">
{row.mountRich ? t.rich(`modes.rows.${idx}.mountRich`, { code }) : row.mount}
</td>
<td className="px-4 py-2">{row.ui}</td>
<td className="px-4 py-2">
{row.useCaseRich ? t.rich(`modes.rows.${idx}.useCaseRich`, { em }) : row.useCase}
</td>
</tr>
))}
</tbody>
</table>
</div>
<Callout variant="info" title={t("modes.bothTitle")}>
{t.rich("modes.bothBody", { code })}
</Callout>
<h2 className="text-2xl font-semibold mt-12 mb-4 text-gray-900">{t("pvesmBranch.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("pvesmBranch.intro", { em })}
</p>
<ol className="list-decimal pl-6 mb-4 text-gray-800 leading-relaxed space-y-2">
{pvesmItems.map((_, idx) => (
<li key={idx}>{t.rich(`pvesmBranch.items.${idx}`, { strong, code })}</li>
))}
</ol>
<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("pvesmBranch.headerType")}</th>
<th className="px-4 py-2 font-semibold">{t("pvesmBranch.headerAllows")}</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-800">
{contentRows.map((row, idx) => (
<tr key={row.type}>
<td className="px-4 py-2 font-mono">{row.type}</td>
<td className="px-4 py-2">
{row.allowsRich
? t.rich(`pvesmBranch.rows.${idx}.allowsRich`, { em, code })
: row.allows}
</td>
</tr>
))}
</tbody>
</table>
</div>
<Callout variant="warning" title={t("pvesmBranch.warnTitle")}>
{t.rich("pvesmBranch.warnBody", { code })}
</Callout>
<p className="mb-4 mt-4 text-gray-800 leading-relaxed">
{t.rich("pvesmBranch.result", { code })}
</p>
<h2 className="text-2xl font-semibold mt-12 mb-4 text-gray-900">{t("fstabBranch.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("fstabBranch.intro", { em, code })}
</p>
<ol className="list-decimal pl-6 mb-4 text-gray-800 leading-relaxed space-y-2">
{fstabItems.map((_, idx) => (
<li key={idx}>{t.rich(`fstabBranch.items.${idx}`, { strong, em, code })}</li>
))}
</ol>
<p className="mb-3 text-gray-800 leading-relaxed">{t("fstabBranch.appliesIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{fstabAppliesItems.map((_, idx) => (
<li key={idx}>{t.rich(`fstabBranch.applies.${idx}`, { code })}</li>
))}
</ul>
<Callout variant="info" title={t("fstabBranch.lxcTitle")}>
{t.rich("fstabBranch.lxcBody", { code, strong, mountLink })}
</Callout>
<Callout variant="warning" title={t("fstabBranch.noUiTitle")}>
{t.rich("fstabBranch.noUiBody", { em })}
</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.pvesmIntro")}</p>
<CopyableCode code={`apt-get install -y nfs-common # one-time: NFS client tools
pvesm add nfs mynfs \\
--server 10.0.0.50 \\
--export /export/proxmox \\
--content import,backup,iso
pvesm status mynfs # verify it's active
ls -la /mnt/pve/mynfs # Proxmox auto-mounts here`} />
<p className="mb-3 mt-6 text-gray-800 leading-relaxed">{t("manual.fstabIntro")}</p>
<CopyableCode code={`apt-get install -y nfs-common # one-time
mkdir -p /mnt/data
mount -t nfs -o "rw,hard,nofail,_netdev,rsize=131072,wsize=131072,timeo=600,retrans=2" \\
10.0.0.50:/export/proxmox /mnt/data
# Persist
echo "10.0.0.50:/export/proxmox /mnt/data nfs rw,hard,nofail,_netdev,rsize=131072,wsize=131072,timeo=600,retrans=2 0 0" \\
>> /etc/fstab
systemctl daemon-reload
# Best-effort open perms for LXC bind-mount writes (server permitting)
chmod 1777 /mnt/data 2>/dev/null || true
setfacl -m o::rwx /mnt/data 2>/dev/null || true
# Bind into an unprivileged LXC (host-side perms only — no changes inside CT)
pct set <ctid> -mp0 /mnt/data,mp=/mnt/data,shared=1,backup=0
pct reboot <ctid>`} />
<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, strong })}</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("test.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("test.body", { code, em })}</p>
<h2 className="text-2xl font-semibold mt-12 mb-4 text-gray-900">{t("troubleshoot.heading")}</h2>
<Callout variant="troubleshoot" title={t("troubleshoot.noServersTitle")}>
{t.rich("troubleshoot.noServersBody", { code, em })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.portTitle")}>
{t.rich("troubleshoot.portBody", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.showmountTitle")}>
{t.rich("troubleshoot.showmountBody", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.inactiveTitle")}>
{t.rich("troubleshoot.inactiveBody", { em, code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.lxcNoWriteTitle")}>
{t.rich("troubleshoot.lxcNoWriteBody", { code, strong })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.fstabBootTitle")}>
{t.rich("troubleshoot.fstabBootBody", { 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, idx) => (
<li key={item.href}>
<Link href={item.href} className="text-blue-600 hover:underline">
{item.label}
</Link>
{item.tailRich ? t.rich(`related.items.${idx}.tailRich`, { code }) : item.tail}
</li>
))}
</ul>
</div>
)
}

View File

@@ -0,0 +1,355 @@
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.hostSamba.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-samba",
},
}
}
type StringItem = string
type ModesRow = { method: string; mount?: string; mountRich?: string; ui: string; useCase?: string; useCaseRich?: string }
type ContentRow = { type: string; allows?: string; allowsRich?: string }
type RelatedItem = { href: string; label: string; tail?: string; tailRich?: string }
export default async function HostSambaPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.storageShare.hostSamba" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { storageShare: { hostSamba: {
modes: { rows: ModesRow[] }
pvesmBranch: { items: StringItem[]; rows: ContentRow[] }
fstabBranch: { items: StringItem[]; applies: StringItem[] }
related: { items: RelatedItem[] }
} } }
}
const modesRows = messages.docs.storageShare.hostSamba.modes.rows
const pvesmItems = messages.docs.storageShare.hostSamba.pvesmBranch.items
const contentRows = messages.docs.storageShare.hostSamba.pvesmBranch.rows
const fstabItems = messages.docs.storageShare.hostSamba.fstabBranch.items
const fstabAppliesItems = messages.docs.storageShare.hostSamba.fstabBranch.applies
const relatedItems = messages.docs.storageShare.hostSamba.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>
const mountLink = (chunks: React.ReactNode) => (
<Link href="/docs/storage-share/lxc-mount-points" className="text-blue-700 hover:underline">{chunks}</Link>
)
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={7}
scriptPath="share/samba_host.sh"
/>
<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("opening.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("opening.body", { strong })}
</p>
<Image
src="/share/host-samba-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.rich("howRuns.body", { code })}
</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 — Discover, validate, choose │
│ (nothing touched yet) │
└──────────────────┬──────────────────────────┘
Server discovery (nmap 139/445 + nmblookup)
Authentication (User or Guest)
Share selection (smbclient -L)
╔═════════════════════════════════════╗
║ MOUNT METHOD PICKER (checklist) ║
║ [ ] As Proxmox storage (pvesm) ║
║ [ ] As host fstab mount only ║
║ (mark one or both — re-prompts ║
║ if you press OK without marks) ║
╚════════════════╤════════════════════╝
┌──────────────┴──────────────┐
▼ ▼
pvesm branch fstab branch
├─ storage ID ├─ mount path
├─ content types ├─ mount options
└─ (User) write
/etc/samba/credentials/...cred
(mode 0600)
▼ ▼
┌─────────────────────────────────────────┐
│ PHASE 2 — Apply (only marked methods) │
└──────────────────┬──────────────────────┘
pvesm add cifs <id> ... + mkdir -p <path>
(auto-mount at mount -t cifs ...
/mnt/pve/<id> with (uid=0,gid=0,
default options) file_mode=0777,
dir_mode=0777)
append /etc/fstab
systemctl daemon-reload
Summary printed`}
</pre>
<h2 className="text-2xl font-semibold mt-12 mb-4 text-gray-900">{t("modes.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("modes.intro")}</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("modes.headerMethod")}</th>
<th className="px-4 py-2 font-semibold">{t("modes.headerMount")}</th>
<th className="px-4 py-2 font-semibold">{t("modes.headerUi")}</th>
<th className="px-4 py-2 font-semibold">{t("modes.headerUseCase")}</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-800 align-top">
{modesRows.map((row, idx) => (
<tr key={idx}>
<td className="px-4 py-2">{t.rich(`modes.rows.${idx}.method`, { strong })}</td>
<td className="px-4 py-2">
{row.mountRich ? t.rich(`modes.rows.${idx}.mountRich`, { code }) : row.mount}
</td>
<td className="px-4 py-2">{row.ui}</td>
<td className="px-4 py-2">
{row.useCaseRich ? t.rich(`modes.rows.${idx}.useCaseRich`, { em }) : row.useCase}
</td>
</tr>
))}
</tbody>
</table>
</div>
<Callout variant="info" title={t("modes.bothTitle")}>
{t.rich("modes.bothBody", { code })}
</Callout>
<h2 className="text-2xl font-semibold mt-12 mb-4 text-gray-900">{t("pvesmBranch.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("pvesmBranch.intro", { em })}
</p>
<ol className="list-decimal pl-6 mb-4 text-gray-800 leading-relaxed space-y-2">
{pvesmItems.map((_, idx) => (
<li key={idx}>{t.rich(`pvesmBranch.items.${idx}`, { strong, em, code })}</li>
))}
</ol>
<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("pvesmBranch.headerType")}</th>
<th className="px-4 py-2 font-semibold">{t("pvesmBranch.headerAllows")}</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-800">
{contentRows.map((row, idx) => (
<tr key={row.type}>
<td className="px-4 py-2 font-mono">{row.type}</td>
<td className="px-4 py-2">
{row.allowsRich
? t.rich(`pvesmBranch.rows.${idx}.allowsRich`, { code, strong })
: row.allows}
</td>
</tr>
))}
</tbody>
</table>
</div>
<Callout variant="warning" title={t("pvesmBranch.warnTitle")}>
{t.rich("pvesmBranch.warnBody", { code })}
</Callout>
<Callout variant="info" title={t("pvesmBranch.credsTitle")}>
{t.rich("pvesmBranch.credsBody", { code })}
</Callout>
<h2 className="text-2xl font-semibold mt-12 mb-4 text-gray-900">{t("fstabBranch.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("fstabBranch.intro", { em, code })}
</p>
<ol className="list-decimal pl-6 mb-4 text-gray-800 leading-relaxed space-y-2">
{fstabItems.map((_, idx) => (
<li key={idx}>{t.rich(`fstabBranch.items.${idx}`, { strong, em, code })}</li>
))}
</ol>
<Callout variant="info" title={t("fstabBranch.credsTitle")}>
{t.rich("fstabBranch.credsBody", { code })}
</Callout>
<p className="mb-3 text-gray-800 leading-relaxed">{t("fstabBranch.appliesIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{fstabAppliesItems.map((_, idx) => (
<li key={idx}>{t.rich(`fstabBranch.applies.${idx}`, { code })}</li>
))}
</ul>
<Callout variant="info" title={t("fstabBranch.lxcTitle")}>
{t.rich("fstabBranch.lxcBody", { code, strong, mountLink })}
</Callout>
<Callout variant="warning" title={t("fstabBranch.noUiTitle")}>
{t.rich("fstabBranch.noUiBody", { em })}
</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.pvesmIntro")}</p>
<CopyableCode code={`apt-get install -y cifs-utils smbclient # one-time: SMB client tools
# with user authentication
pvesm add cifs mycifs \\
--server 10.0.0.50 \\
--share proxmox \\
--username backup_user \\
--password 's3cret' \\
--content backup,iso
# guest access (no credentials)
pvesm add cifs mycifs-guest \\
--server 10.0.0.50 \\
--share public \\
--content iso,vztmpl
pvesm status mycifs # verify it's active
ls -la /mnt/pve/mycifs # Proxmox auto-mounts here`} />
<p className="mb-3 mt-6 text-gray-800 leading-relaxed">{t("manual.fstabUserIntro")}</p>
<CopyableCode code={`# 1. credentials file (root-only)
mkdir -p /etc/samba/credentials && chmod 0700 /etc/samba/credentials
cat > /etc/samba/credentials/nas01_share.cred <<'EOF'
username=admin
password=s3cret
EOF
chmod 0600 /etc/samba/credentials/nas01_share.cred
# 2. mount with open uid/gid/file_mode (for unpriv LXC bind-mounts)
mkdir -p /mnt/data
mount -t cifs //10.0.0.50/share /mnt/data \\
-o "rw,uid=0,gid=0,file_mode=0777,dir_mode=0777,iocharset=utf8,nofail,_netdev,credentials=/etc/samba/credentials/nas01_share.cred"
# 3. persist
echo "//10.0.0.50/share /mnt/data cifs rw,uid=0,gid=0,file_mode=0777,dir_mode=0777,iocharset=utf8,nofail,_netdev,credentials=/etc/samba/credentials/nas01_share.cred 0 0" \\
>> /etc/fstab
systemctl daemon-reload
# 4. bind into an unpriv LXC (no changes inside the CT)
pct set <ctid> -mp0 /mnt/data,mp=/mnt/data,shared=1,backup=0
pct reboot <ctid>`} />
<p className="mb-3 mt-6 text-gray-800 leading-relaxed">{t("manual.fstabGuestIntro")}</p>
<CopyableCode code={`mkdir -p /mnt/public
mount -t cifs //10.0.0.50/public /mnt/public \\
-o "rw,uid=0,gid=0,file_mode=0777,dir_mode=0777,iocharset=utf8,nofail,_netdev,guest"
echo "//10.0.0.50/public /mnt/public cifs rw,uid=0,gid=0,file_mode=0777,dir_mode=0777,iocharset=utf8,nofail,_netdev,guest 0 0" \\
>> /etc/fstab
systemctl daemon-reload`} />
<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, em, strong })}</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("test.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("test.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.noServersTitle")}>
{t.rich("troubleshoot.noServersBody", { code, em })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.noSharesTitle")}>
{t.rich("troubleshoot.noSharesBody", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.denyTitle")}>
{t.rich("troubleshoot.denyBody", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.sleepTitle")}>
{t.rich("troubleshoot.sleepBody", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.lxcNoWriteTitle")}>
{t.rich("troubleshoot.lxcNoWriteBody", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.fstabBootTitle")}>
{t.rich("troubleshoot.fstabBootBody", { 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, idx) => (
<li key={item.href}>
<Link href={item.href} className="text-blue-600 hover:underline">
{item.label}
</Link>
{item.tailRich ? t.rich(`related.items.${idx}.tailRich`, { code }) : item.tail}
</li>
))}
</ul>
</div>
)
}

View File

@@ -0,0 +1,422 @@
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 { DataFlowDiagram } from "@/components/ui/data-flow-diagram"
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.lxcMountPoints.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/lxc-mount-points",
},
}
}
type StringItem = string
type SourceRow = { source: string; where?: string; whereRich?: string; labelRich: string }
type StringList = string[]
type RelatedItem = {
href: string
label: string
extraHref?: string
extraLabel?: string
joiner?: string
tail?: string
tailRich?: string
}
export default async function LxcMountPointsPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.storageShare.lxcMountPoints" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { storageShare: { lxcMountPoints: {
bigPicture: { items: StringItem[] }
sources: { rows: SourceRow[] }
troubleshoot: { nfsItems: StringList }
related: { items: RelatedItem[] }
} } }
}
const bigPictureItems = messages.docs.storageShare.lxcMountPoints.bigPicture.items
const sourceRows = messages.docs.storageShare.lxcMountPoints.sources.rows
const nfsItems = messages.docs.storageShare.lxcMountPoints.troubleshoot.nfsItems
const relatedItems = messages.docs.storageShare.lxcMountPoints.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/lxc-mount-manager_minimal.sh"
/>
<Callout variant="info" title={t("intro.title")}>
{t("intro.body")}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("bigPicture.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("bigPicture.intro", { code, em })}
</p>
<DataFlowDiagram
nodes={[
{ label: t("bigPicture.sourceLabel"), detail: t("bigPicture.sourceDetail"), variant: "source" },
{ label: t("bigPicture.targetLabel"), detail: t("bigPicture.targetDetail"), variant: "target" },
]}
arrowLabel={t("bigPicture.arrowLabel")}
bidirectional
command={`# What the script writes:
pct set <ctid> -mpN /mnt/data, mp=/mnt/data, shared=1, backup=0`}
/>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("bigPicture.outro", { code })}
</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{bigPictureItems.map((_, idx) => (
<li key={idx}>{t.rich(`bigPicture.items.${idx}`, { code })}</li>
))}
</ul>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("perms.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("perms.intro", { strong, em })}
</p>
<div className="overflow-x-auto my-6 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 align-top">{t("perms.headerType")}</th>
<th className="px-4 py-2 font-semibold align-top">{t("perms.headerAction")}</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-800 align-top">
<tr>
<td className="px-4 py-3">
<div className="font-semibold whitespace-nowrap">{t("perms.localType")}</div>
<div className="text-xs text-gray-600 mt-1 font-mono">{t("perms.localTypeSub")}</div>
</td>
<td className="px-4 py-3">{t.rich("perms.localActionRich", { code })}</td>
</tr>
<tr>
<td className="px-4 py-3">
<div className="font-semibold whitespace-nowrap">{t("perms.cifsType")}</div>
<div className="text-xs text-gray-600 mt-1 font-mono">{t("perms.cifsTypeSub")}</div>
</td>
<td className="px-4 py-3">{t.rich("perms.cifsActionRich", { code })}</td>
</tr>
<tr>
<td className="px-4 py-3">
<div className="font-semibold whitespace-nowrap">{t("perms.nfsType")}</div>
<div className="text-xs text-gray-600 mt-1 font-mono">{t("perms.nfsTypeSub")}</div>
</td>
<td className="px-4 py-3">{t.rich("perms.nfsActionRich", { code })}</td>
</tr>
</tbody>
</table>
</div>
<Callout variant="info" title={t("perms.privTitle")}>
{t("perms.privBody")}
</Callout>
<Callout variant="warning" title={t("perms.noCtTitle")}>
{t.rich("perms.noCtBody", { strong, code })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("writes.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("writes.intro", { code, strong })}
</p>
<CopyableCode
code={`# /etc/pve/lxc/545.conf — single line added by the script
mp0: /mnt/NAS/hdd_cache,mp=/mnt/NAS/hdd_cache,shared=1,backup=0`}
className="my-4"
/>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("writes.outro", { em })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("writes.twoWaysHeading")}</h3>
<div className="overflow-x-auto my-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 align-top">{t("writes.headerApproach")}</th>
<th className="px-4 py-2 font-semibold align-top">{t("writes.headerChanges")}</th>
<th className="px-4 py-2 font-semibold align-top">{t("writes.headerWhen")}</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-800 align-top">
<tr>
<td className="px-4 py-3 whitespace-nowrap">
<div className="font-semibold">{t("writes.hostType")}</div>
<div className="text-xs text-gray-600 mt-1">{t("writes.hostTypeSub")}</div>
</td>
<td className="px-4 py-3">{t.rich("writes.hostChangesRich", { code, em })}</td>
<td className="px-4 py-3">{t("writes.hostWhen")}</td>
</tr>
<tr>
<td className="px-4 py-3 whitespace-nowrap">
<div className="font-semibold">{t.rich("writes.idmapTypeRich", { code })}</div>
<div className="text-xs text-gray-600 mt-1">{t("writes.idmapTypeSub")}</div>
</td>
<td className="px-4 py-3">{t.rich("writes.idmapChangesRich", { code })}</td>
<td className="px-4 py-3">{t.rich("writes.idmapWhenRich", { em, code })}</td>
</tr>
</tbody>
</table>
</div>
<Callout variant="tip" title={t("writes.idmapTipTitle")}>
{t.rich("writes.idmapTipBody", { code })}
</Callout>
<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/lxc-mount-points-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("addFlow.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("addFlow.intro")}</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 — Pick CT, host dir, mount point │
│ (nothing touched yet) │
└──────────────────┬──────────────────────────┘
pct list — pick the target container
Unified host-directory picker
Lists every candidate the script can detect:
├─ Mounted CIFS / NFS shares (/proc/mounts)
├─ fstab-inactive network mounts (defined
│ but not currently mounted) — labelled
│ "fstab(off)-"
├─ Local /mnt/* directories
├─ Proxmox-managed storages under /mnt/pve/*
│ (NFS / CIFS shares registered via pvesm)
│ — labelled "PVE-"
└─ "Enter path manually" for anything else
Detect the host directory TYPE
└─ local / cifs / nfs
(drives the permission-fix branch later)
Container mount point picker
├─ Create new directory in /mnt
│ (auto-suggests basename of host dir)
├─ Enter manual path (must be absolute)
└─ Cancel
Validates the path is not already used as
a mount point in this CT.
Detect CT type:
├─ Privileged → no UID shift
└─ Unprivileged → +100000 (default idmap)
ACTIVE FIX FOR THE HOST DIRECTORY
(depends on the type detected earlier)
├─ cifs → offer remount with open uid/gid
├─ nfs → offer chmod + setfacl on share
└─ local → handled AFTER the bind mount
(only if CT is unprivileged)
┌──────── Cancel OR Confirm ────┐
▼ ▼
Exit, nothing ┌─────────────────┴─────────────────┐
was changed │ PHASE 2 — Apply │
└─────────────────┬─────────────────┘
Find next free mpN slot
(scans /etc/pve/lxc/<ctid>.conf)
pct set <ctid> -mpN \\
<host-dir>,
mp=<container-path>,
shared=1, backup=0
For local + unprivileged:
└─ lmm_offer_host_permissions
(chmod o+rwx + ACL on host dir,
only if perms were insufficient)
Offer to restart the container
└─ pct reboot <ctid>
(mounts only become active on
the next CT start)
Verify: pct exec <ctid> -- test -d
<container-path> → "accessible"`}
</pre>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("sources.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("sources.intro", { em })}
</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("sources.headerSource")}</th>
<th className="px-4 py-2 font-semibold">{t("sources.headerWhere")}</th>
<th className="px-4 py-2 font-semibold">{t("sources.headerLabel")}</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-800">
{sourceRows.map((row, idx) => (
<tr key={row.source}>
<td className="px-4 py-2 font-semibold">{row.source}</td>
<td className="px-4 py-2">
{row.whereRich ? t.rich(`sources.rows.${idx}.whereRich`, { code }) : row.where}
</td>
<td className="px-4 py-2">{t.rich(`sources.rows.${idx}.labelRich`, { code, em })}</td>
</tr>
))}
</tbody>
</table>
</div>
<Callout variant="tip" title={t("sources.tipTitle")}>
{t.rich("sources.tipBody", { 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.privIntro")}</p>
<CopyableCode code={`# 1. add the bind mount to the CT config
pct set 101 -mp0 /mnt/data,mp=/mnt/data,shared=1,backup=0
# 2. restart the CT to activate the mount
pct reboot 101
# 3. verify from inside
pct exec 101 -- ls -la /mnt/data`} />
<p className="mb-3 mt-6 text-gray-800 leading-relaxed">{t("manual.unprivLocalIntro")}</p>
<CopyableCode code={`# host: open the directory for any mapped UID
chmod o+rwx /mnt/data
setfacl -m o::rwx /mnt/data
setfacl -m d:o::rwx /mnt/data # default ACL = applies to new files
# add the bind mount + restart
pct set 102 -mp0 /mnt/data,mp=/mnt/data,shared=1,backup=0
pct reboot 102`} />
<p className="mb-3 mt-6 text-gray-800 leading-relaxed">{t("manual.unprivCifsIntro")}</p>
<CopyableCode code={`# host: remount the CIFS with open uid/gid
umount /mnt/pve/cifs-nas
mount -t cifs //10.0.0.50/share /mnt/pve/cifs-nas \\
-o "username=user,password=pass,uid=0,gid=0,file_mode=0777,dir_mode=0777"
# update /etc/fstab if the mount is persistent
sed -i 's|^\\(//10.0.0.50/share .*cifs \\).*|\\1username=user,password=pass,uid=0,gid=0,file_mode=0777,dir_mode=0777 0 0|' /etc/fstab
# bind mount + restart
pct set 102 -mp0 /mnt/pve/cifs-nas,mp=/mnt/nas,shared=1,backup=0
pct reboot 102`} />
<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.rich("remove.warnBody", { code })}
</Callout>
<h2 className="text-2xl font-semibold mt-12 mb-4 text-gray-900">{t("troubleshoot.heading")}</h2>
<Callout variant="troubleshoot" title={t("troubleshoot.noMountTitle")}>
{t.rich("troubleshoot.noMountBody", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.noWriteTitle")}>
{t.rich("troubleshoot.noWriteBody", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.alreadyTitle")}>
{t("troubleshoot.alreadyBody")}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.nfsTitle")}>
{t("troubleshoot.nfsIntro")}
<ul className="mt-2 list-disc list-inside space-y-1">
{nfsItems.map((_, idx) => (
<li key={idx}>{t.rich(`troubleshoot.nfsItems.${idx}`, { code })}</li>
))}
</ul>
{t("troubleshoot.nfsOutro")}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.fstabOffTitle")}>
{t.rich("troubleshoot.fstabOffBody", { 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, idx) => (
<li key={item.href}>
<Link href={item.href} className="text-blue-600 hover:underline">
{item.label}
</Link>
{item.extraHref && item.extraLabel && (
<>
{item.joiner}
<Link href={item.extraHref} className="text-blue-600 hover:underline">
{item.extraLabel}
</Link>
</>
)}
{item.tailRich ? t.rich(`related.items.${idx}.tailRich`, { code }) : item.tail}
</li>
))}
</ul>
</div>
)
}

View File

@@ -0,0 +1,332 @@
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 { DataFlowDiagram } from "@/components/ui/data-flow-diagram"
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.lxcNfsClient.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/lxc-nfs-client",
},
}
}
type FlagRow = { flag: string; effect?: string; effectRich?: string }
type StringItem = string
type RelatedItem = { href: string; label: string; tail?: string; tailRich?: string }
export default async function LxcNfsClientPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.storageShare.lxcNfsClient" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { storageShare: { lxcNfsClient: {
fstabFlags: { rows: FlagRow[] }
troubleshoot: { aptItems: StringItem[]; squashItems: StringItem[] }
related: { items: RelatedItem[] }
} } }
}
const flagRows = messages.docs.storageShare.lxcNfsClient.fstabFlags.rows
const aptItems = messages.docs.storageShare.lxcNfsClient.troubleshoot.aptItems
const squashItems = messages.docs.storageShare.lxcNfsClient.troubleshoot.squashItems
const relatedItems = messages.docs.storageShare.lxcNfsClient.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>
const hostNfsLink = (chunks: React.ReactNode) => (
<Link href="/docs/storage-share/host-nfs" className="text-blue-700 hover:underline">{chunks}</Link>
)
const mountLink = (chunks: React.ReactNode) => (
<Link href="/docs/storage-share/lxc-mount-points" className="text-blue-700 hover:underline">{chunks}</Link>
)
const importLink = (chunks: React.ReactNode) => (
<Link href="/docs/disk-manager/import-disk-lxc" className="text-blue-700 hover:underline">{chunks}</Link>
)
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={8}
scriptPath="share/nfs_client.sh"
/>
<Callout variant="warning" title={t("privReq.title")}>
{t.rich("privReq.body", { code, strong, hostNfsLink, mountLink })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("what.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("what.body", { em, strong, code })}
</p>
<DataFlowDiagram
nodes={[
{ label: t("what.diagramServerLabel"), detail: t("what.diagramServerDetail"), variant: "source" },
{ label: t("what.diagramHostLabel"), detail: t("what.diagramHostDetail"), variant: "bridge" },
{ label: t("what.diagramCtLabel"), detail: t("what.diagramCtDetail"), variant: "target" },
]}
arrowLabel={t("what.diagramArrow")}
command={`# Inside the CT — what the script writes:
pct exec <ctid> -- mount -t nfs -o rw,hard,rsize=…,wsize=… \\
<server>:/export/data /mnt/data
# Persistent (added to /etc/fstab inside the CT):
<server>:/export/data /mnt/data nfs <opts>,_netdev,x-systemd.automount,noauto 0 0`}
/>
<Callout variant="info" title={t("what.twoWaysTitle")}>
<ul className="mt-2 list-disc list-inside space-y-1">
<li>{t.rich("what.twoWaysBind", { strong, mountLink })}</li>
<li>{t.rich("what.twoWaysDirect", { strong })}</li>
</ul>
</Callout>
<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/lxc-nfs-client-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>
<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 — Pick CT, server, export, options │
│ (nothing touched yet) │
└──────────────────┬──────────────────────────┘
Privileged-CT gate (share-common.func)
├─ pct list — pick CT
├─ Auto-start if stopped
└─ Reads /etc/pve/lxc/<ctid>.conf
└─ unprivileged: 1 → abort with help message
Install NFS client packages (in CT)
└─ pct exec apt-get install -y nfs-common
(skipped if nfs-common is already installed)
Verifies: showmount + mount.nfs both present
Server selection
├─ Auto-discover (nmap from HOST on /24,
│ port 2049, then showmount -e per result)
└─ Manual: type IP or hostname
Reachability validation chain (from inside CT)
├─ pct exec ping -c 1 -W 3 <server> ── fail → abort
├─ pct exec nc -z -w 3 <server> 2049 ── fail → abort
└─ pct exec showmount -e <server> ── fail → abort
Export selection
├─ Server returns exports → checklist with ACL
└─ No exports / blocked → manual input
Validate the chosen export still exists
(re-runs showmount -e | grep <export>)
Mount-point picker (3 options)
├─ 1. Create new folder in /mnt
│ (default: nfs_<server>_<export-basename>)
├─ 2. Select existing folder in /mnt
│ (warns if folder is not empty —
│ mounting hides existing files)
└─ 3. Enter custom path
Mount-options preset (3 options)
├─ 1. Read/write
│ rw,hard,rsize=1048576,wsize=1048576,
│ timeo=600,retrans=2
├─ 2. Read-only
│ ro,hard,rsize=1048576,wsize=1048576,
│ timeo=600,retrans=2
└─ 3. Custom — type your own option string
Permanent mount? (yes/no)
└─ yes → write entry to /etc/fstab
┌──────── Cancel OR Confirm ────┐
▼ ▼
Exit, nothing ┌─────────────────┴─────────────────┐
was changed │ PHASE 2 — Mount and persist │
└─────────────────┬─────────────────┘
Create mount point if missing
(pct exec mkdir -p <path>)
If something is already mounted there,
offer to unmount first
pct exec mount -t nfs \\
-o <chosen options> \\
<server>:<export> <mount-point>
Smoke test: write a 0-byte file
(.test_write) and delete it
└─ no write access → "read-only"
If "permanent" was chosen:
└─ Append to /etc/fstab inside CT:
<srv>:<exp> <mp> nfs \\
<opts>,_netdev,
x-systemd.automount,noauto 0 0
(any prior entry for this MP is removed first)
Print summary (server / export / mp /
options / permanent yes-no)`}
</pre>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("fstabFlags.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("fstabFlags.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("fstabFlags.headerFlag")}</th>
<th className="px-4 py-2 font-semibold">{t("fstabFlags.headerEffect")}</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-800">
{flagRows.map((row, idx) => (
<tr key={row.flag}>
<td className="px-4 py-2 font-mono">{row.flag}</td>
<td className="px-4 py-2">
{row.effectRich ? t.rich(`fstabFlags.rows.${idx}.effectRich`, { code }) : row.effect}
</td>
</tr>
))}
</tbody>
</table>
</div>
<Callout variant="info" title={t("fstabFlags.netEffectTitle")}>
{t.rich("fstabFlags.netEffectBody", { em })}
</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.rich("manual.body", { strong, code })}
</p>
<CopyableCode code={`# 1. install the NFS client (one-time)
apt-get update
apt-get install -y nfs-common
# 2. test reachability
ping -c 1 -W 3 10.0.0.50
nc -z -w 3 10.0.0.50 2049
showmount -e 10.0.0.50
# 3. mount it (one-shot)
mkdir -p /mnt/data
mount -t nfs -o "rw,hard,rsize=1048576,wsize=1048576,timeo=600,retrans=2" \\
10.0.0.50:/export/data /mnt/data
# 4. make it permanent (safe boot defaults)
cat >> /etc/fstab <<EOF
10.0.0.50:/export/data /mnt/data nfs rw,hard,rsize=1048576,wsize=1048576,timeo=600,retrans=2,_netdev,x-systemd.automount,noauto 0 0
EOF
systemctl daemon-reload`} />
<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("unmount.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("unmount.body", { strong, em, code })}
</p>
<Callout variant="warning" title={t("unmount.warnTitle")}>
{t.rich("unmount.warnBody", { code, em })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("test.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("test.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.privTitle")}>
{t.rich("troubleshoot.privBody", { code, importLink })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.aptTitle")}>
{t.rich("troubleshoot.aptIntro", { code })}
<ul className="mt-2 list-disc list-inside space-y-1">
{aptItems.map((_, idx) => (
<li key={idx}>{t.rich(`troubleshoot.aptItems.${idx}`, { code })}</li>
))}
</ul>
{t("troubleshoot.aptOutro")}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.portTitle")}>
{t.rich("troubleshoot.portBody", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.bootTitle")}>
{t.rich("troubleshoot.bootBody", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.squashTitle")}>
{t.rich("troubleshoot.squashIntro", { code })}
<ul className="mt-2 list-disc list-inside space-y-1">
{squashItems.map((_, idx) => (
<li key={idx}>{t.rich(`troubleshoot.squashItems.${idx}`, { code })}</li>
))}
</ul>
{t("troubleshoot.squashOutro")}
</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, idx) => (
<li key={item.href}>
<Link href={item.href} className="text-blue-600 hover:underline">
{item.label}
</Link>
{item.tailRich ? t.rich(`related.items.${idx}.tailRich`, { em }) : item.tail}
</li>
))}
</ul>
</div>
)
}

View File

@@ -0,0 +1,350 @@
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 { DataFlowDiagram } from "@/components/ui/data-flow-diagram"
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.lxcNfsServer.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/lxc-nfs-server",
},
}
}
type NetworkRow = { mode: string; value: string; when?: string; whenRich?: string }
type OptionRow = { option: string; effect?: string; effectRich?: string }
type StringItem = string
type RelatedItem = { href: string; label: string; tail?: string; tailRich?: string }
export default async function LxcNfsServerPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.storageShare.lxcNfsServer" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { storageShare: { lxcNfsServer: {
network: { rows: NetworkRow[] }
options: { rows: OptionRow[] }
troubleshoot: { aptItems: StringItem[]; ownItems: StringItem[] }
related: { items: RelatedItem[] }
} } }
}
const networkRows = messages.docs.storageShare.lxcNfsServer.network.rows
const optionRows = messages.docs.storageShare.lxcNfsServer.options.rows
const aptItems = messages.docs.storageShare.lxcNfsServer.troubleshoot.aptItems
const ownItems = messages.docs.storageShare.lxcNfsServer.troubleshoot.ownItems
const relatedItems = messages.docs.storageShare.lxcNfsServer.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={12}
scriptPath="share/nfs_lxc_server.sh"
/>
<Callout variant="warning" title={t("privReq.title")}>
{t.rich("privReq.body", { code, strong })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("what.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("what.body", { em, strong })}
</p>
<DataFlowDiagram
nodes={[
{ label: t("what.diagramServerLabel"), detail: t("what.diagramServerDetail"), variant: "source" },
{ label: t("what.diagramClientLabel"), detail: t("what.diagramClientDetail"), variant: "target" },
]}
arrowLabel={t("what.diagramArrow")}
bidirectional
command={`# /etc/exports inside the CT:
/mnt/data <network>(rw,sync,no_subtree_check,no_root_squash)`}
/>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("shared.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("shared.body", { code, strong })}
</p>
<Callout variant="info" title={t("shared.gidTitle")}>
{t.rich("shared.gidBody", { strong, code })}
</Callout>
<Callout variant="warning" title={t("shared.remapTitle")}>
{t.rich("shared.remapBody", { code, strong })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("defaults.heading")}</h2>
<Callout variant="danger" title={t("defaults.warnTitle")}>
{t.rich("defaults.warnBody", { code, strong })}
</Callout>
<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/lxc-nfs-server-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>
<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 — Pick CT, folder, network, opts │
│ (nothing touched yet) │
└──────────────────┬──────────────────────────┘
Privileged-CT gate (share-common.func)
├─ pct list — pick CT
├─ Auto-start if stopped
└─ Aborts if "unprivileged: 1" in CT config
Folder selection (2 modes)
├─ Auto: choose from existing folders
│ inside /mnt of the CT
└─ Manual: enter any absolute path
(must already exist inside the CT)
Network ACL (3 modes)
├─ 1. Local network (192.168.0.0/16)
├─ 2. Custom subnet (e.g. 192.168.10.0/24)
└─ 3. Single host IP
Export options (3 modes)
├─ 1. Read-write — rw,sync,no_subtree_check,
│ no_root_squash (DEFAULT)
├─ 2. Read-only — ro,sync,no_subtree_check,
│ no_root_squash
└─ 3. Custom — type your own option string
┌──────── Cancel OR Confirm ────┐
▼ ▼
Exit, nothing ┌─────────────────┴─────────────────┐
was changed │ PHASE 2 — Install + configure │
└─────────────────┬─────────────────┘
Install NFS server (in CT)
└─ pct exec apt-get install -y \\
nfs-kernel-server
nfs-common rpcbind
+ systemctl enable --now both
(skipped if already installed)
setup_universal_sharedfiles_group
└─ groupadd -g 101000 sharedfiles
(or groupmod if exists at wrong GID)
For each regular user (UID >= 1000):
├─ usermod -a -G sharedfiles <user>
└─ useradd -u <uid+100000> \\
-g sharedfiles \\
remap_<uid>
Same for common UIDs (33, 1000-1002)
Apply ownership + SGID on the folder
└─ chown root:sharedfiles <folder>
chmod 2775 <folder>
(sticky group: new files inherit
the sharedfiles group)
Update /etc/exports
└─ If existing entry for the folder:
ask "update?", remove + replace.
Else:
append the new line.
systemctl restart rpcbind \\
nfs-kernel-server
exportfs -ra
Print connection details:
• Server IP (CT hostname -I)
• Export path
• Mount options chosen
• Network ACL
• Mount examples (auto / v4 / v3)`}
</pre>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("network.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("network.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("network.headerMode")}</th>
<th className="px-4 py-2 font-semibold">{t("network.headerValue")}</th>
<th className="px-4 py-2 font-semibold">{t("network.headerWhen")}</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-800">
{networkRows.map((row, idx) => (
<tr key={row.mode}>
<td className="px-4 py-2 font-semibold">{row.mode}</td>
<td className="px-4 py-2 font-mono">{row.value}</td>
<td className="px-4 py-2">
{row.whenRich ? t.rich(`network.rows.${idx}.whenRich`, { code }) : row.when}
</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("options.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">{t("options.headerOption")}</th>
<th className="px-4 py-2 font-semibold">{t("options.headerEffect")}</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-800">
{optionRows.map((row, idx) => (
<tr key={row.option}>
<td className="px-4 py-2 font-mono">{row.option}</td>
<td className="px-4 py-2">
{row.effectRich ? t.rich(`options.rows.${idx}.effectRich`, { code, strong }) : row.effect}
</td>
</tr>
))}
</tbody>
</table>
</div>
<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.rich("manual.body", { strong, code })}
</p>
<CopyableCode code={`# 1. install the NFS server (one-time)
apt-get update
apt-get install -y nfs-kernel-server nfs-common rpcbind
systemctl enable --now rpcbind nfs-kernel-server
# 2. create the sharedfiles group convention
groupadd -g 101000 sharedfiles
# add each regular user to it
for u in $(awk -F: '$3 >= 1000 && $3 < 65534 {print $1}' /etc/passwd); do
usermod -a -G sharedfiles "$u"
done
# 3. set ownership + SGID on the folder
mkdir -p /mnt/data
chown root:sharedfiles /mnt/data
chmod 2775 /mnt/data # SGID: new files inherit group
# 4. add the export line
echo "/mnt/data 192.168.0.0/16(rw,sync,no_subtree_check,no_root_squash)" \\
>> /etc/exports
# 5. apply
systemctl restart rpcbind nfs-kernel-server
exportfs -ra
# verify
exportfs -v
showmount -e localhost`} />
<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("delete.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("delete.body", { code })}</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("status.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("status.body", { code })}</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("uninstall.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("uninstall.body", { code, strong })}
</p>
<Callout variant="warning" title={t("uninstall.warnTitle")}>
{t.rich("uninstall.warnBody", { em, code })}
</Callout>
<h2 className="text-2xl font-semibold mt-12 mb-4 text-gray-900">{t("troubleshoot.heading")}</h2>
<Callout variant="troubleshoot" title={t("troubleshoot.privTitle")}>
{t.rich("troubleshoot.privBody", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.aptTitle")}>
{t("troubleshoot.aptIntro")}
<ul className="mt-2 list-disc list-inside space-y-1">
{aptItems.map((_, idx) => (
<li key={idx}>{t.rich(`troubleshoot.aptItems.${idx}`, { code })}</li>
))}
</ul>
{t("troubleshoot.aptOutro")}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.aclTitle")}>
{t.rich("troubleshoot.aclBody", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.ownTitle")}>
{t("troubleshoot.ownIntro")}
<ul className="mt-2 list-disc list-inside space-y-1">
{ownItems.map((_, idx) => (
<li key={idx}>{t.rich(`troubleshoot.ownItems.${idx}`, { code })}</li>
))}
</ul>
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.noShowTitle")}>
{t.rich("troubleshoot.noShowBody", { 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, idx) => (
<li key={item.href}>
<Link href={item.href} className="text-blue-600 hover:underline">
{item.label}
</Link>
{item.tailRich ? t.rich(`related.items.${idx}.tailRich`, { em }) : item.tail}
</li>
))}
</ul>
</div>
)
}

View File

@@ -0,0 +1,379 @@
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 { DataFlowDiagram } from "@/components/ui/data-flow-diagram"
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.lxcSambaClient.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/lxc-samba-client",
},
}
}
type OptionRow = { option: string; effect: string }
type StringItem = string
type RelatedItem = { href: string; label: string; tail?: string; tailRich?: string }
export default async function LxcSambaClientPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.storageShare.lxcSambaClient" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { storageShare: { lxcSambaClient: {
options: { rows: OptionRow[] }
troubleshoot: { aptItems: StringItem[] }
related: { items: RelatedItem[] }
} } }
}
const optionRows = messages.docs.storageShare.lxcSambaClient.options.rows
const aptItems = messages.docs.storageShare.lxcSambaClient.troubleshoot.aptItems
const relatedItems = messages.docs.storageShare.lxcSambaClient.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>
const hostSambaLink = (chunks: React.ReactNode) => (
<Link href="/docs/storage-share/host-samba" className="text-blue-700 hover:underline">{chunks}</Link>
)
const mountLink = (chunks: React.ReactNode) => (
<Link href="/docs/storage-share/lxc-mount-points" className="text-blue-700 hover:underline">{chunks}</Link>
)
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={10}
scriptPath="share/samba_client.sh"
/>
<Callout variant="warning" title={t("privReq.title")}>
{t.rich("privReq.body", { code, strong, hostSambaLink, mountLink })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("what.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("what.body")}</p>
<DataFlowDiagram
nodes={[
{ label: t("what.diagramServerLabel"), detail: t("what.diagramServerDetail"), variant: "source" },
{ label: t("what.diagramHostLabel"), detail: t("what.diagramHostDetail"), variant: "bridge" },
{ label: t("what.diagramCtLabel"), detail: t("what.diagramCtDetail"), variant: "target" },
]}
arrowLabel={t("what.diagramArrow")}
command={`# Credentials stored in the CT (root:0600):
# /etc/samba/credentials/<server>_<share>.cred
# What the script writes inside the CT:
pct exec <ctid> -- mount -t cifs //<server>/<share> /mnt/share \\
-o "rw,file_mode=0664,dir_mode=0775,iocharset=utf8,
credentials=/etc/samba/credentials/<srv>_<sh>.cred"`}
/>
<Callout variant="info" title={t("what.twoWaysTitle")}>
<ul className="mt-2 list-disc list-inside space-y-1">
<li>{t.rich("what.twoWaysBind", { strong, mountLink })}</li>
<li>{t.rich("what.twoWaysDirect", { strong })}</li>
</ul>
</Callout>
<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/lxc-samba-client-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>
<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 — Pick CT, server, auth, share │
│ (nothing touched yet) │
└──────────────────┬──────────────────────────┘
Privileged-CT gate (share-common.func)
├─ pct list — pick CT
├─ Auto-start if stopped
└─ Reads /etc/pve/lxc/<ctid>.conf
└─ unprivileged: 1 → abort with help message
Install Samba client packages (in CT)
└─ pct exec apt-get install -y \\
cifs-utils smbclient
(skipped if already installed)
Verifies: smbclient + mount.cifs both present
Creates /etc/samba/credentials (mode 0700)
Server selection (3 modes)
├─ Auto-discover (nmap from HOST on /24,
│ ports 139/445, then nmblookup -A
│ for NetBIOS names → "NETBIOS (ip)")
├─ Manual: type IP or hostname
└─ Recent: parses /etc/fstab for previously
used CIFS servers (one-click selection)
Authentication (2 modes)
├─ User + password
│ ├─ Username (whiptail inputbox)
│ ├─ Password (passwordbox, hidden)
│ ├─ Confirm password
│ └─ ACTIVE VALIDATION against the server:
│ creates a temp credentials file,
│ runs smbclient -L with -A,
│ distinguishes "guest fallback" from
│ real auth success, retries on failure
└─ Guest: validate guest access first
(smbclient -L -N must succeed)
Share selection
├─ Server returns shares → menu
│ (filters out IPC$, ADMIN$, print$;
│ for guest: only shares the user
│ confirmed accessible during validation)
└─ No shares / blocked → manual input
Validate the chosen share still exists
Mount-point picker (3 options)
├─ 1. Create new folder in /mnt
│ (default: same name as the share)
├─ 2. Select existing folder in /mnt
└─ 3. Enter custom path
Mount-options preset (3 options)
├─ 1. Read/write
│ rw,file_mode=0664,dir_mode=0775,
│ iocharset=utf8
├─ 2. Read-only
│ ro,file_mode=0444,dir_mode=0555,
│ iocharset=utf8
└─ 3. Custom — type your own option string
Permanent mount? (yes/no)
└─ yes → write entry to /etc/fstab
┌──────── Cancel OR Confirm ────┐
▼ ▼
Exit, nothing ┌─────────────────┴─────────────────┐
was changed │ PHASE 2 — Mount and persist │
└─────────────────┬─────────────────┘
Create mount point if missing
(pct exec mkdir -p <path>)
If something is already mounted there,
offer to unmount first
For user auth: write credentials file
/etc/samba/credentials/<srv>_<sh>.cred
(root:0600 inside the CT)
pct exec mount -t cifs \\
//<server>/<share> <mp> \\
-o <opts>,credentials=<file>
(or -o <opts>,guest for guest)
Smoke test: write a 0-byte file
and delete it (.test_write)
└─ no write access → "read-only"
If "permanent" was chosen:
└─ Append to /etc/fstab inside CT:
//<srv>/<sh> <mp> cifs \\
<opts>,credentials=…,
_netdev,
x-systemd.automount,noauto 0 0
(any prior entry for this mp is removed first)
Print summary (server / share / mp /
auth mode / permanent yes-no)`}
</pre>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("creds.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("creds.body", { strong, code })}
</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">
{`Path: /etc/samba/credentials/<server>_<share>.cred
Owner: root
Mode: 0600
Content:
username=<your-username>
password=<your-password>
Reference in /etc/fstab:
//<server>/<share> /mnt/<path> cifs rw,...,
credentials=/etc/samba/credentials/<server>_<share>.cred,
_netdev,x-systemd.automount,noauto 0 0`}
</pre>
<Callout variant="info" title={t("creds.whyTitle")}>
{t.rich("creds.whyBody", { code })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("options.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">{t("options.headerOption")}</th>
<th className="px-4 py-2 font-semibold">{t("options.headerEffect")}</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-800">
{optionRows.map((row) => (
<tr key={row.option}>
<td className="px-4 py-2 font-mono">{row.option}</td>
<td className="px-4 py-2">{row.effect}</td>
</tr>
))}
</tbody>
</table>
</div>
<Callout variant="info" title={t("options.netEffectTitle")}>
{t.rich("options.netEffectBody", { em })}
</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.rich("manual.body", { strong, code })}
</p>
<CopyableCode code={`# 1. install the Samba client (one-time)
apt-get update
apt-get install -y cifs-utils smbclient
# 2. test reachability
ping -c 1 -W 3 10.0.0.50
nc -z -w 3 10.0.0.50 445
smbclient -L 10.0.0.50 -U user
# 3. write the credentials file (root-only)
mkdir -p /etc/samba/credentials
chmod 700 /etc/samba/credentials
cat > /etc/samba/credentials/10.0.0.50_share.cred <<EOF
username=user
password=s3cret
EOF
chmod 600 /etc/samba/credentials/10.0.0.50_share.cred
# 4. mount it (one-shot)
mkdir -p /mnt/share
mount -t cifs //10.0.0.50/share /mnt/share \\
-o "rw,file_mode=0664,dir_mode=0775,iocharset=utf8,credentials=/etc/samba/credentials/10.0.0.50_share.cred"
# 5. make it permanent (safe boot defaults)
cat >> /etc/fstab <<EOF
//10.0.0.50/share /mnt/share cifs rw,file_mode=0664,dir_mode=0775,iocharset=utf8,credentials=/etc/samba/credentials/10.0.0.50_share.cred,_netdev,x-systemd.automount,noauto 0 0
EOF
systemctl daemon-reload`} />
<p className="mb-3 mt-6 text-gray-800 leading-relaxed">{t("manual.guestIntro")}</p>
<CopyableCode code={`mount -t cifs //10.0.0.50/public /mnt/public \\
-o "rw,file_mode=0664,dir_mode=0775,iocharset=utf8,guest"
# fstab equivalent
echo "//10.0.0.50/public /mnt/public cifs rw,file_mode=0664,dir_mode=0775,iocharset=utf8,guest,_netdev,x-systemd.automount,noauto 0 0" \\
>> /etc/fstab`} />
<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("unmount.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("unmount.body", { strong, code })}
</p>
<Callout variant="warning" title={t("unmount.warnTitle")}>
{t.rich("unmount.warnBody", { code, em })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("test.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("test.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.privTitle")}>
{t.rich("troubleshoot.privBody", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.aptTitle")}>
{t.rich("troubleshoot.aptIntro", { code })}
<ul className="mt-2 list-disc list-inside space-y-1">
{aptItems.map((_, idx) => (
<li key={idx}>{t.rich(`troubleshoot.aptItems.${idx}`, { code })}</li>
))}
</ul>
{t("troubleshoot.aptOutro")}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.guestFallbackTitle")}>
{t("troubleshoot.guestFallbackBody")}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.denyTitle")}>
{t.rich("troubleshoot.denyBody", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.utf8Title")}>
{t.rich("troubleshoot.utf8Body", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.bootTitle")}>
{t.rich("troubleshoot.bootBody", { 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, idx) => (
<li key={item.href}>
<Link href={item.href} className="text-blue-600 hover:underline">
{item.label}
</Link>
{item.tailRich ? t.rich(`related.items.${idx}.tailRich`, { em }) : item.tail}
</li>
))}
</ul>
</div>
)
}

View File

@@ -0,0 +1,395 @@
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 { DataFlowDiagram } from "@/components/ui/data-flow-diagram"
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.lxcSambaServer.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/lxc-samba-server",
},
}
}
type StringItem = string
type RelatedItem = { href: string; label: string; tail?: string; tailRich?: string }
export default async function LxcSambaServerPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.storageShare.lxcSambaServer" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { storageShare: { lxcSambaServer: {
troubleshoot: { aptItems: StringItem[] }
related: { items: RelatedItem[] }
} } }
}
const aptItems = messages.docs.storageShare.lxcSambaServer.troubleshoot.aptItems
const relatedItems = messages.docs.storageShare.lxcSambaServer.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>
const nfsLink = (chunks: React.ReactNode) => (
<Link href="/docs/storage-share/lxc-nfs-server" className="text-blue-700 hover:underline">{chunks}</Link>
)
const clientLink = (chunks: React.ReactNode) => (
<Link href="/docs/storage-share/lxc-samba-client" className="text-blue-600 hover:underline">{chunks}</Link>
)
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={12}
scriptPath="share/samba_lxc_server.sh"
/>
<Callout variant="warning" title={t("privReq.title")}>
{t.rich("privReq.body", { code, strong })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("what.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("what.body", { code })}
</p>
<DataFlowDiagram
nodes={[
{ label: t("what.diagramServerLabel"), detail: t("what.diagramServerDetail"), variant: "source" },
{ label: t("what.diagramClientLabel"), detail: t("what.diagramClientDetail"), variant: "target" },
]}
arrowLabel={t("what.diagramArrow")}
bidirectional
command={`# /etc/samba/smb.conf — block written by ProxMenux:
[<share-name>]
path = /mnt/data
valid users = <username>
force group = sharedfiles
read only = no
create mask = 0664
directory mask = 2775`}
/>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("perms.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("perms.body", { code, strong })}
</p>
<div className="overflow-x-auto my-6 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 align-top">{t("perms.headerType")}</th>
<th className="px-4 py-2 font-semibold align-top">{t("perms.headerAction")}</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-800 align-top">
<tr>
<td className="px-4 py-3">
<div className="font-semibold whitespace-nowrap">{t("perms.bindType")}</div>
<div className="text-xs text-gray-600 mt-1">{t.rich("perms.bindTypeSubRich", { code })}</div>
</td>
<td className="px-4 py-3">{t.rich("perms.bindActionRich", { code })}</td>
</tr>
<tr>
<td className="px-4 py-3">
<div className="font-semibold whitespace-nowrap">{t("perms.localType")}</div>
<div className="text-xs text-gray-600 mt-1">{t("perms.localTypeSub")}</div>
</td>
<td className="px-4 py-3">{t.rich("perms.localActionRich", { code })}</td>
</tr>
</tbody>
</table>
</div>
<Callout variant="warning" title={t("perms.gidTitle")}>
{t.rich("perms.gidBody", { strong, code, nfsLink })}
</Callout>
<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/lxc-samba-server-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>
<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 — Pick CT, folder, user, options │
│ (nothing touched yet) │
└──────────────────┬──────────────────────────┘
Privileged-CT gate (share-common.func)
├─ pct list — pick CT
├─ Auto-start if stopped
└─ Aborts if "unprivileged: 1" in CT config
Folder selection (2 modes)
├─ Auto: choose from /mnt/* in the CT
└─ Manual: enter any absolute path
(offers to mkdir -p if missing)
Samba install check
├─ Already installed?
│ └─ Detect existing user via pdbedit -L
└─ First time?
├─ apt-get install samba samba-common-bin acl
├─ Ask username (default: "proxmenux")
├─ Ask password (twice — must match)
├─ adduser <username> (no password)
└─ smbpasswd -a <username>
Permission setup (2 paths)
├─ Bind-mount detected
│ groupadd -g 999 sharedfiles
│ usermod -aG sharedfiles <user>
│ chown root:sharedfiles + chmod 2775
│ setfacl fallback if write fails
└─ Local folder
chown -R <user>:<user>
chmod -R 755
setfacl fallback if needed
Share permissions (3 modes)
├─ rw — read-write block (default)
├─ ro — read-only block
└─ custom — your own directives
┌──────── Cancel OR Confirm ────┐
▼ ▼
Exit, nothing ┌─────────────────┴─────────────────┐
was changed │ PHASE 2 — Write smb.conf + apply │
└─────────────────┬─────────────────┘
If [share-name] already in smb.conf:
└─ ask "update?", remove + replace
(sed deletes from [name] to next blank)
Else:
└─ append the new block
systemctl restart smbd.service
Print connection details:
• Server IP (hostname -I)
• Share name + path
• Username
• Sample mount commands`}
</pre>
<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.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("modes.headerMode")}</th>
<th className="px-4 py-2 font-semibold">{t.rich("modes.headerBlock", { code })}</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-800 align-top">
<tr>
<td className="px-4 py-3 font-semibold whitespace-nowrap">{t("modes.rwMode")}</td>
<td className="px-4 py-3">
<pre className="text-xs font-mono">{`read only = no
writable = yes
browseable = yes
guest ok = no
create mask = 0664
directory mask = 2775
force create mode = 0664
force directory mode = 2775`}</pre>
</td>
</tr>
<tr>
<td className="px-4 py-3 font-semibold whitespace-nowrap">{t("modes.roMode")}</td>
<td className="px-4 py-3">
<pre className="text-xs font-mono">{`read only = yes
writable = no
browseable = yes
guest ok = no`}</pre>
</td>
</tr>
<tr>
<td className="px-4 py-3 font-semibold">{t("modes.customMode")}</td>
<td className="px-4 py-3">
{t.rich("modes.customBodyRich", { code })}
</td>
</tr>
</tbody>
</table>
</div>
<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.rich("manual.body", { strong, code })}
</p>
<CopyableCode code={`# 1. install Samba (one-time)
apt-get update
apt-get install -y samba samba-common-bin acl
# 2. create a Samba user (system + smbpasswd)
adduser --disabled-password --gecos "" proxmenux
echo -e "P4ssw0rd\\nP4ssw0rd" | smbpasswd -a proxmenux
# 3. for a bind-mounted folder: shared group + SGID
mkdir -p /mnt/data
groupadd -g 999 sharedfiles 2>/dev/null || true
usermod -aG sharedfiles proxmenux
chown root:sharedfiles /mnt/data
chmod 2775 /mnt/data
# fallback if user can't write:
# setfacl -R -m u:proxmenux:rwx /mnt/data
# 4. write the share block
cat >> /etc/samba/smb.conf <<'EOF'
[data]
path = /mnt/data
valid users = proxmenux
force group = sharedfiles
read only = no
writable = yes
browseable = yes
guest ok = no
create mask = 0664
directory mask = 2775
force create mode = 0664
force directory mode = 2775
veto files = /lost+found/
EOF
# 5. apply
systemctl restart smbd
testparm -s | grep -A6 '^\\[data\\]'`} />
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("connect.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">{t("connect.headerOs")}</th>
<th className="px-4 py-2 font-semibold">{t("connect.headerHow")}</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-800 align-top">
<tr>
<td className="px-4 py-3 font-semibold">{t("connect.windowsOs")}</td>
<td className="px-4 py-3">{t.rich("connect.windowsHowRich", { code, em })}</td>
</tr>
<tr>
<td className="px-4 py-3 font-semibold">{t("connect.macosOs")}</td>
<td className="px-4 py-3">{t.rich("connect.macosHowRich", { code, em })}</td>
</tr>
<tr>
<td className="px-4 py-3 font-semibold">{t("connect.linuxOs")}</td>
<td className="px-4 py-3">{t.rich("connect.linuxHowRich", { code, clientLink })}</td>
</tr>
</tbody>
</table>
</div>
<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("delete.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("delete.body", { code })}</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("status.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("status.body", { code })}</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("uninstall.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("uninstall.body", { code, strong })}
</p>
<Callout variant="warning" title={t("uninstall.warnTitle")}>
{t.rich("uninstall.warnBody", { em, code })}
</Callout>
<h2 className="text-2xl font-semibold mt-12 mb-4 text-gray-900">{t("troubleshoot.heading")}</h2>
<Callout variant="troubleshoot" title={t("troubleshoot.privTitle")}>
{t.rich("troubleshoot.privBody", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.aptTitle")}>
{t("troubleshoot.aptIntro")}
<ul className="mt-2 list-disc list-inside space-y-1">
{aptItems.map((_, idx) => (
<li key={idx}>{t.rich(`troubleshoot.aptItems.${idx}`, { code })}</li>
))}
</ul>
{t("troubleshoot.aptOutro")}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.noShareTitle")}>
{t.rich("troubleshoot.noShareBody", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.authTitle")}>
{t.rich("troubleshoot.authBody", { em, code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.groupTitle")}>
{t.rich("troubleshoot.groupBody", { code })}
</Callout>
<Callout variant="troubleshoot" title={t("troubleshoot.bothTitle")}>
{t.rich("troubleshoot.bothBody", { code })}
<pre className="mt-2 p-2 rounded bg-white/50 text-xs overflow-x-auto"><code>groupmod -g 101000 sharedfiles
chgrp -R sharedfiles /mnt/&lt;your-share&gt;</code></pre>
</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, idx) => (
<li key={item.href}>
<Link href={item.href} className="text-blue-600 hover:underline">
{item.label}
</Link>
{item.tailRich ? t.rich(`related.items.${idx}.tailRich`, { em }) : item.tail}
</li>
))}
</ul>
</div>
)
}

View File

@@ -0,0 +1,269 @@
import type { Metadata } from "next"
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
import { Link } from "@/i18n/navigation"
import Image from "next/image"
import {
ArrowRight,
HardDrive,
Server,
Network,
FolderOpen,
Database,
Share2,
Download,
Upload,
Link2,
} from "lucide-react"
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.storageShare.meta" })
return {
title: t("title"),
description: t("description"),
keywords: [
"proxmox nfs",
"proxmox samba",
"proxmox cifs",
"proxmox iscsi",
"proxmox lxc mount points",
"proxmox bind mount",
"proxmox shared storage",
"proxmox storage share",
"proxmox nfs server lxc",
"proxmox samba server lxc",
],
alternates: { canonical: "https://proxmenux.com/docs/storage-share" },
openGraph: {
title: t("ogTitle"),
description: t("ogDescription"),
type: "article",
url: "https://proxmenux.com/docs/storage-share",
},
twitter: {
card: "summary",
title: t("twitterTitle"),
description: t("twitterDescription"),
},
}
}
type OptionData = { href: string; icon: string; title: string; description: string }
type StringItem = string
const ICONS: Record<string, React.ComponentType<{ className?: string; "aria-hidden"?: boolean }>> = {
Network,
Share2,
Database,
HardDrive,
FolderOpen,
Download,
Upload,
Link2,
}
function OptionCard({ option }: { option: OptionData }) {
const Icon = ICONS[option.icon] || Network
return (
<Link
href={option.href}
className="group flex items-start gap-3 rounded-md border border-gray-200 bg-white p-3 transition-colors hover:border-blue-400 hover:bg-blue-50"
>
<span className="inline-flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md bg-gray-100 text-gray-600 group-hover:bg-blue-100 group-hover:text-blue-700">
<Icon className="h-4 w-4" aria-hidden />
</span>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1 text-sm font-medium text-gray-900 group-hover:text-blue-700">
{option.title}
<ArrowRight className="h-3.5 w-3.5 text-gray-400 group-hover:text-blue-600 transition-transform group-hover:translate-x-0.5" />
</div>
<div className="mt-0.5 text-xs text-gray-600 leading-snug">{option.description}</div>
</div>
</Link>
)
}
export default async function StorageShareOverviewPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.storageShare" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { storageShare: {
groups: { hostItems: StringItem[]; lxcMountItems: StringItem[]; lxcNetItems: StringItem[] }
host: { options: OptionData[] }
lxcNet: { options: OptionData[] }
} }
}
const hostItems = messages.docs.storageShare.groups.hostItems
const lxcMountItems = messages.docs.storageShare.groups.lxcMountItems
const lxcNetItems = messages.docs.storageShare.groups.lxcNetItems
const hostOptions = messages.docs.storageShare.host.options
const lxcNetOptions = messages.docs.storageShare.lxcNet.options
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 mountLink = (chunks: React.ReactNode) => (
<Link href="/docs/storage-share/lxc-mount-points" className="text-blue-700 hover:underline">{chunks}</Link>
)
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={5}
scriptPath="menus/share_menu.sh"
/>
<Callout variant="info" title={t("intro.title")}>
{t.rich("intro.body", { em, strong })}
</Callout>
<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/storage-share-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("groups.heading")}</h2>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("groups.intro", { strong, em })}
</p>
<div className="grid gap-4 md:grid-cols-1 lg:grid-cols-3 mb-8 not-prose">
<a
href="#host"
className="rounded-lg border-2 border-blue-300 bg-blue-50 p-5 flex flex-col transition-shadow hover:shadow-md"
>
<div className="flex items-center gap-3 mb-3">
<span className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-blue-100 text-blue-700">
<Server className="h-5 w-5" aria-hidden />
</span>
<h3 className="text-lg font-semibold text-gray-900 m-0">{t("groups.hostTitle")}</h3>
</div>
<p className="text-sm text-gray-800 mb-3">{t.rich("groups.hostBody", { code })}</p>
<ul className="space-y-1 text-sm text-gray-700 list-disc pl-5 mb-0 marker:text-gray-400">
{hostItems.map((_, idx) => (
<li key={idx}>{t(`groups.hostItems.${idx}`)}</li>
))}
</ul>
</a>
<a
href="#lxc-mount"
className="rounded-lg border-2 border-emerald-300 bg-emerald-50 p-5 flex flex-col transition-shadow hover:shadow-md"
>
<div className="flex items-center gap-3 mb-3">
<span className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-emerald-100 text-emerald-700">
<Link2 className="h-5 w-5" aria-hidden />
</span>
<h3 className="text-lg font-semibold text-gray-900 m-0">{t("groups.lxcMountTitle")}</h3>
</div>
<p className="text-sm text-gray-800 mb-3">{t.rich("groups.lxcMountBody", { code })}</p>
<ul className="space-y-1 text-sm text-gray-700 list-disc pl-5 mb-0 marker:text-gray-400">
{lxcMountItems.map((_, idx) => (
<li key={idx}>{t.rich(`groups.lxcMountItems.${idx}`, { code })}</li>
))}
</ul>
</a>
<a
href="#lxc-net"
className="rounded-lg border-2 border-amber-300 bg-amber-50 p-5 flex flex-col transition-shadow hover:shadow-md"
>
<div className="flex items-center gap-3 mb-3">
<span className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-amber-100 text-amber-700">
<Network className="h-5 w-5" aria-hidden />
</span>
<h3 className="text-lg font-semibold text-gray-900 m-0">{t("groups.lxcNetTitle")}</h3>
</div>
<p className="text-sm text-gray-800 mb-3">{t.rich("groups.lxcNetBody", { strong })}</p>
<ul className="space-y-1 text-sm text-gray-700 list-disc pl-5 mb-0 marker:text-gray-400">
{lxcNetItems.map((_, idx) => (
<li key={idx}>{t.rich(`groups.lxcNetItems.${idx}`, { strong })}</li>
))}
</ul>
</a>
</div>
<h2 id="host" className="text-2xl font-semibold mt-10 mb-4 text-gray-900 scroll-mt-24">{t("host.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("host.intro", { code, strong })}
</p>
<div className="grid gap-3 md:grid-cols-2 mb-8 not-prose">
{hostOptions.map((o) => (
<OptionCard key={o.href} option={o} />
))}
</div>
<h2 id="lxc-mount" className="text-2xl font-semibold mt-10 mb-4 text-gray-900 scroll-mt-24">{t("lxcMount.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("lxcMount.intro", { code })}
</p>
<div className="grid gap-3 md:grid-cols-2 mb-8 not-prose">
<OptionCard
option={{
title: t("lxcMount.card.title"),
description: t("lxcMount.card.description"),
icon: "Link2",
href: "/docs/storage-share/lxc-mount-points",
}}
/>
</div>
<h2 id="lxc-net" className="text-2xl font-semibold mt-10 mb-4 text-gray-900 scroll-mt-24">{t("lxcNet.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("lxcNet.intro", { em, mountLink })}
</p>
<div className="grid gap-3 md:grid-cols-2 mb-8 not-prose">
{lxcNetOptions.map((o) => (
<OptionCard key={o.href} option={o} />
))}
</div>
<Callout variant="warning" title={t("privReq.title")}>
{t.rich("privReq.body", { strong, code, mountLink })}
</Callout>
<Callout variant="info" title={t("unprivExplain.title")}>
{t.rich("unprivExplain.body", { strong, em, code })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("scripts.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("scripts.intro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
<li>
<a
href="https://github.com/MacRimi/ProxMenux/blob/main/scripts/global/share-common.func"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline font-mono"
>
global/share-common.func
</a>
{t("scripts.itemTail")}
</li>
</ul>
</div>
)
}