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,518 @@
import type { Metadata } from "next"
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
import { Link } from "@/i18n/navigation"
import { ExternalLink } from "lucide-react"
import { DocHeader } from "@/components/ui/doc-header"
import { Callout } from "@/components/ui/callout"
import CopyableCode from "@/components/CopyableCode"
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "docs.monitor.dashboard.hardware.meta" })
return { title: t("title"), description: t("description") }
}
type GpuTool = { vendor: string; tool: string; projectLabel: string; projectHref?: string }
type DataRow = { section: string; endpoint: string; source: string }
type WhereNextItem = { label: string; href: string; tail: string }
export default async function HardwareTabPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.monitor.dashboard.hardware" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { monitor: { dashboard: { hardware: {
thresholds: { items: string[] }
sections: { systemInfoItems: string[]; thermalItems: string[] }
graphics: { tools: GpuTool[]; whereGoItems: string[] }
coral: { pathsItems: string[] }
power: { items: string[] }
dataCollected: { rows: DataRow[] }
whereNext: { items: WhereNextItem[] }
} } } }
}
const hw = messages.docs.monitor.dashboard.hardware
const thresholdsItems = hw.thresholds.items
const systemInfoItems = hw.sections.systemInfoItems
const thermalItems = hw.sections.thermalItems
const gpuTools = hw.graphics.tools
const whereGoItems = hw.graphics.whereGoItems
const coralPathsItems = hw.coral.pathsItems
const powerItems = hw.power.items
const dataRows = hw.dataCollected.rows
const whereNextItems = hw.whereNext.items
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
const green = () => <span className="inline-block w-2.5 h-2.5 rounded-full bg-green-500 align-middle mr-1" />
const amber = () => <span className="inline-block w-2.5 h-2.5 rounded-full bg-amber-500 align-middle mr-1" />
const red = () => <span className="inline-block w-2.5 h-2.5 rounded-full bg-red-500 align-middle mr-1" />
const thresholdsLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/dashboard/settings#status-colours" className="text-blue-700 hover:underline">
{chunks}
</Link>
)
const switchModeLink = (chunks: React.ReactNode) => (
<Link href="/docs/hardware/switch-gpu-mode" className="text-blue-600 hover:underline">{chunks}</Link>
)
const nvidiaHostLink = (chunks: React.ReactNode) => (
<Link href="/docs/hardware/nvidia-host" className="text-blue-600 hover:underline">{chunks}</Link>
)
const nvidiaAnchor = (chunks: React.ReactNode) => (
<a
href="https://www.nvidia.com/Download/index.aspx"
target="_blank"
rel="noopener noreferrer"
className="text-amber-900 underline hover:no-underline inline-flex items-center gap-1"
>
{chunks}
<ExternalLink className="h-3.5 w-3.5" aria-hidden="true" />
</a>
)
const link1 = (chunks: React.ReactNode) => (
<Link href="/docs/hardware/nvidia-host" className="text-blue-600 hover:underline">{chunks}</Link>
)
const link2 = (chunks: React.ReactNode) => (
<Link href="/docs/hardware/switch-gpu-mode" className="text-blue-600 hover:underline">{chunks}</Link>
)
const link3 = (chunks: React.ReactNode) => (
<Link href="/docs/hardware/gpu-vm-passthrough" className="text-blue-600 hover:underline">{chunks}</Link>
)
const link4 = (chunks: React.ReactNode) => (
<Link href="/docs/hardware/igpu-acceleration-lxc" className="text-blue-600 hover:underline">{chunks}</Link>
)
const installLink = (chunks: React.ReactNode) => (
<Link href="/docs/hardware/install-coral-tpu-host" className="text-blue-600 hover:underline">{chunks}</Link>
)
const lxcLink = (chunks: React.ReactNode) => (
<Link href="/docs/hardware/coral-tpu-lxc" className="text-blue-600 hover:underline">{chunks}</Link>
)
const coralAnchor = (chunks: React.ReactNode) => (
<a
href="https://coral.ai/docs/m2/get-started/"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline inline-flex items-center gap-1"
>
{chunks}
<ExternalLink className="h-3.5 w-3.5" aria-hidden="true" />
</a>
)
const storageLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/dashboard/storage" className="text-blue-600 hover:underline">{chunks}</Link>
)
const smartLink = (chunks: React.ReactNode) => (
<Link href="/docs/disk-manager/smart-disk-test" className="text-blue-600 hover:underline">{chunks}</Link>
)
const pciSwitchLink = (chunks: React.ReactNode) => (
<Link href="/docs/hardware/switch-gpu-mode" className="text-blue-600 hover:underline">{chunks}</Link>
)
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={14}
/>
<Callout variant="info" title={t("intro.title")}>
{t.rich("intro.body", { code })}
</Callout>
<Callout variant="tip" title={t("thresholds.title")}>
{t.rich("thresholds.intro", { strong, green, amber, red })}
<ul className="list-disc pl-6 mt-2 space-y-0.5">
{thresholdsItems.map((_, idx) => (
<li key={idx}>{t.rich(`thresholds.items.${idx}`, { strong })}</li>
))}
</ul>
{t.rich("thresholds.outro", { link: thresholdsLink })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("sections.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("sections.intro", { em })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("sections.systemInfoTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">{t("sections.systemInfoIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{systemInfoItems.map((_, idx) => (
<li key={idx}>{t.rich(`sections.systemInfoItems.${idx}`, { strong })}</li>
))}
</ul>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("sections.memoryTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("sections.memoryBody", { code })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("sections.thermalTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("sections.thermalIntro", { code })}
</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{thermalItems.map((_, idx) => (
<li key={idx}>{t.rich(`sections.thermalItems.${idx}`, { strong, code })}</li>
))}
</ul>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("graphics.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("graphics.intro", { em, strong, code, link: switchModeLink })}
</p>
<figure className="my-6">
<img
src="/monitor/hardware/hw-graphics-cards-vfio.png"
alt={t("graphics.vfioImageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("graphics.vfioImageCaption", { code })}
</figcaption>
</figure>
<figure className="my-6">
<img
src="/monitor/hardware/hw-graphics-cards-lxc.png"
alt={t("graphics.lxcImageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("graphics.lxcImageCaption")}
</figcaption>
</figure>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("graphics.realtimeTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("graphics.realtimeBody", { code })}
</p>
<p className="mb-4 text-gray-800 leading-relaxed">{t("graphics.toolsIntro")}</p>
<div className="overflow-x-auto mb-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">{t("graphics.headerVendor")}</th>
<th className="px-4 py-2 font-semibold">{t("graphics.headerTool")}</th>
<th className="px-4 py-2 font-semibold">{t("graphics.headerProject")}</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200 text-gray-800">
{gpuTools.map((row) => (
<tr key={row.vendor}>
<td className="px-4 py-2 align-top whitespace-nowrap"><strong>{row.vendor}</strong></td>
<td className="px-4 py-2 align-top"><code>{row.tool}</code></td>
<td className="px-4 py-2 align-top">
{row.projectHref ? (
<a
href={row.projectHref}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline inline-flex items-center gap-1"
>
{row.projectLabel}
<ExternalLink className="h-3.5 w-3.5" aria-hidden="true" />
</a>
) : (
row.projectLabel
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<figure className="my-6">
<img
src="/monitor/hardware/hw-gpu-nvidia-modal.png"
alt={t("graphics.nvidiaImageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("graphics.nvidiaImageCaption")}
</figcaption>
</figure>
<figure className="my-6">
<img
src="/monitor/hardware/hw-gpu-intel-modal.png"
alt={t("graphics.intelImageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("graphics.intelImageCaption", { code })}
</figcaption>
</figure>
<figure className="my-6">
<img
src="/monitor/hardware/hw-gpu-amd-modal.png"
alt={t("graphics.amdImageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("graphics.amdImageCaption", { code })}
</figcaption>
</figure>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("graphics.installTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("graphics.installBody", { code, strong, link: nvidiaHostLink })}
</p>
<figure className="my-6">
<img
src="/monitor/hardware/hw-gpu-nvidia-no-driver.png"
alt={t("graphics.noDriverAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("graphics.noDriverCaption")}
</figcaption>
</figure>
<figure className="my-6">
<img
src="/monitor/hardware/hw-gpu-nvidia-install-prompt.png"
alt={t("graphics.promptAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("graphics.promptCaption")}
</figcaption>
</figure>
<figure className="my-6">
<img
src="/monitor/hardware/hw-gpu-nvidia-install-success.png"
alt={t("graphics.successAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("graphics.successCaption", { code })}
</figcaption>
</figure>
<Callout variant="warning" title={t("graphics.warningTitle")}>
{t.rich("graphics.warningBody", { code, em, a: nvidiaAnchor })}
</Callout>
<p className="mt-4 mb-2 text-gray-800 leading-relaxed">{t("graphics.whereGoIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{whereGoItems.map((_, idx) => (
<li key={idx}>{t.rich(`graphics.whereGoItems.${idx}`, { em, link1, link2, link3, link4 })}</li>
))}
</ul>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("coral.heading")} <em>{t("coral.subHeading")}</em>
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("coral.intro", { code })}
</p>
<figure className="my-6">
<img
src="/monitor/hardware/hw-coral-tpu-modal.png"
alt={t("coral.imageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("coral.imageCaption")}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">{t("coral.pathsIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{coralPathsItems.map((_, idx) => (
<li key={idx}>{t.rich(`coral.pathsItems.${idx}`, { strong, code })}</li>
))}
</ul>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("coral.outro", { installLink, lxcLink, a: coralAnchor })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("storage.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("storage.intro", { code, em })}
</p>
<figure className="my-6">
<img
src="/monitor/hardware/hw-storage-summary.png"
alt={t("storage.imageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("storage.imageCaption", { em })}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("storage.nvmeBody", { strong })}
</p>
<figure className="my-6">
<img
src="/monitor/hardware/hw-storage-modal-nvme.png"
alt={t("storage.nvmeModalAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("storage.nvmeModalCaption")}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("storage.outro", { em, storageLink, smartLink })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("pci.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("pci.intro", { strong, em, code })}
</p>
<figure className="my-6">
<img
src="/monitor/hardware/hw-pci-devices.png"
alt={t("pci.imageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("pci.imageCaption", { code })}
</figcaption>
</figure>
<Callout variant="tip" title={t("pci.bdfTitle")}>
{t.rich("pci.bdfBody", { code, link: pciSwitchLink })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("usb.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("usb.intro", { code, em })}
</p>
<figure className="my-6">
<img
src="/monitor/hardware/hw-usb-devices.png"
alt={t("usb.imageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("usb.imageCaption", { code })}
</figcaption>
</figure>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("power.heading")} <em>{t("power.subHeading")}</em>
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("power.intro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{powerItems.map((_, idx) => (
<li key={idx}>{t.rich(`power.items.${idx}`, { strong })}</li>
))}
</ul>
<figure className="my-6">
<img
src="/monitor/hardware/hw-power-supplies.png"
alt={t("power.supplyImageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("power.supplyImageCaption")}
</figcaption>
</figure>
<figure className="my-6">
<img
src="/monitor/hardware/hw-cpu-power.png"
alt={t("power.cpuImageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("power.cpuImageCaption")}
</figcaption>
</figure>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("psu.heading")} <em>{t("psu.subHeading")}</em>
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("psu.body")}</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("fans.heading")} <em>{t("fans.subHeading")}</em>
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("fans.body")}</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
{t("ups.heading")} <em>{t("ups.subHeading")}</em>
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("ups.body", { em })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("dataCollected.heading")}</h2>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerSection")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerEndpoint")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerSource")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{dataRows.map((row, idx) => (
<tr key={row.section} className={idx < dataRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top">{row.section}</td>
<td className="px-3 py-2 align-top font-mono text-xs">{row.endpoint}</td>
<td className="px-3 py-2 align-top">{t.rich(`dataCollected.rows.${idx}.source`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<CopyableCode
code={`${t("dataCollected.codeComment1")}
lspci -nnk | grep -A2 -E 'VGA|Audio|Network|3D'
sensors
${t("dataCollected.codeComment2")}
curl -H "Authorization: Bearer <token>" \\
http://<host>:8008/api/hardware | jq '.gpus'`}
className="my-4"
/>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("whereNext.heading")}</h2>
<ul className="list-disc pl-6 text-gray-800 leading-relaxed space-y-1">
{whereNextItems.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,325 @@
import type { Metadata } from "next"
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
import { Link } from "@/i18n/navigation"
import { Download } from "lucide-react"
import { DocHeader } from "@/components/ui/doc-header"
import { Callout } from "@/components/ui/callout"
import CopyableCode from "@/components/CopyableCode"
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "docs.monitor.dashboard.network.meta" })
return { title: t("title"), description: t("description") }
}
type TopRow = { card: string; what: string }
type DrillRow = { block: string; contents: string }
type ThresholdRow = { status: string; range: string; impact: string }
type DataRow = { section: string; endpoint: string; source: string }
type WhereNextItem = { label: string; href: string; tail: string }
export default async function NetworkTabPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.monitor.dashboard.network" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { monitor: { dashboard: { network: {
topRow: { rows: TopRow[] }
groups: { badges: string[] }
drillIn: { rows: DrillRow[] }
latency: {
targets: string[]
mode2Items: string[]
thresholdRows: ThresholdRow[]
sections: string[]
}
dataCollected: { rows: DataRow[] }
whereNext: { items: WhereNextItem[] }
} } } }
}
const net = messages.docs.monitor.dashboard.network
const topRows = net.topRow.rows
const badges = net.groups.badges
const drillRows = net.drillIn.rows
const targets = net.latency.targets
const mode2Items = net.latency.mode2Items
const thresholdRows = net.latency.thresholdRows
const sections = net.latency.sections
const dataRows = net.dataCollected.rows
const whereNextItems = net.whereNext.items
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={13}
/>
<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("topRow.heading")}</h2>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("topRow.headerCard")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("topRow.headerWhat")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{topRows.map((row, idx) => (
<tr key={row.card} className={idx < topRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.card}</strong></td>
<td className="px-3 py-2 align-top">
{t.rich(`topRow.rows.${idx}.what`, { em, strong })}
</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("groups.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("groups.intro", { strong })}
</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{badges.map((_, idx) => (
<li key={idx}>{t.rich(`groups.badges.${idx}`, { strong, code })}</li>
))}
</ul>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("groups.clickable", { strong })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("groups.physicalTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("groups.physicalBody", { code, strong, em })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("groups.bridgeTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("groups.bridgeBody", { code, strong })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("groups.vmTitle")}</h3>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("groups.vmBody", { code, em })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("drillIn.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("drillIn.intro")}</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("drillIn.headerBlock")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("drillIn.headerContents")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{drillRows.map((row, idx) => (
<tr key={row.block} className={idx < drillRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.block}</strong></td>
<td className="px-3 py-2 align-top">
{t.rich(`drillIn.rows.${idx}.contents`, { code })}
</td>
</tr>
))}
</tbody>
</table>
</div>
<Callout variant="tip" title={t("drillIn.inactiveTitle")}>
{t.rich("drillIn.inactiveBody", { em })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("latency.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("latency.intro", { em })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("latency.targetsTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">{t("latency.targetsIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{targets.map((_, idx) => (
<li key={idx}>{t.rich(`latency.targets.${idx}`, { strong })}</li>
))}
</ul>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("latency.mode1Title")}</h3>
<figure className="my-4">
<img
src="/monitor/network-latency-historical.png"
alt={t("latency.mode1Alt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("latency.mode1Caption")}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("latency.mode1Body1", { em })}
</p>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("latency.mode1Body2", { code })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("latency.mode2Title")}</h3>
<figure className="my-4">
<img
src="/monitor/network-latency-realtime.png"
alt={t("latency.mode2Alt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("latency.mode2Caption", { em })}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">{t("latency.mode2Intro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{mode2Items.map((_, idx) => (
<li key={idx}>{t.rich(`latency.mode2Items.${idx}`, { strong, code })}</li>
))}
</ul>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("latency.thresholdsTitle")}</h3>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("latency.headerStatus")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("latency.headerRange")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("latency.headerImpact")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{thresholdRows.map((row, idx) => (
<tr key={row.status} className={idx < thresholdRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.status}</strong></td>
<td className="px-3 py-2 align-top">{row.range}</td>
<td className="px-3 py-2 align-top">{row.impact}</td>
</tr>
))}
</tbody>
</table>
</div>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("latency.reportTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("latency.reportIntro", { strong })}
</p>
<figure className="my-6">
<img
src="/monitor/network-latency-report-preview.png"
alt={t("latency.reportPreviewAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("latency.reportPreviewCaption")}
</figcaption>
</figure>
<div className="my-6">
<a
href="/monitor/sample-network-latency-report.pdf"
download
className="inline-flex items-center gap-2 px-4 py-2 rounded-md border border-blue-200 bg-blue-50 text-blue-700 font-medium hover:bg-blue-100 transition-colors"
>
<Download className="h-4 w-4" aria-hidden="true" />
{t("latency.downloadLabel")}
</a>
</div>
<p className="mb-4 text-gray-800 leading-relaxed">{t("latency.sectionsIntro")}</p>
<ol className="list-decimal pl-6 mb-6 text-gray-800 leading-relaxed space-y-1">
{sections.map((_, idx) => (
<li key={idx}>{t.rich(`latency.sections.${idx}`, { strong })}</li>
))}
</ol>
<Callout variant="tip" title={t("latency.useCaseTitle")}>
{t.rich("latency.useCaseBody", { em })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("excluding.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("excluding.body1", { code })}
</p>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("excluding.body2", { strong, em })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("dataCollected.heading")}</h2>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerSection")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerEndpoint")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerSource")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{dataRows.map((row, idx) => (
<tr key={row.section} className={idx < dataRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top">{row.section}</td>
<td className="px-3 py-2 align-top font-mono text-xs">{row.endpoint}</td>
<td className="px-3 py-2 align-top">
{t.rich(`dataCollected.rows.${idx}.source`, { code })}
</td>
</tr>
))}
</tbody>
</table>
</div>
<CopyableCode
code={`${t("dataCollected.codeComment1")}
ip -br link
ip -br addr
${t("dataCollected.codeComment2")}
curl -H "Authorization: Bearer <token>" \\
http://<host>:8008/api/network/latency/current | jq`}
className="my-4"
/>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("whereNext.heading")}</h2>
<ul className="list-disc pl-6 text-gray-800 leading-relaxed space-y-1">
{whereNextItems.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,120 @@
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"
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "docs.monitor.dashboard.meta" })
return {
title: t("title"),
description: t("description"),
}
}
type TabRow = { name: string; linksTo?: string; owns: string }
type WhereNextItem = { label: string; href: string; tail: string }
export default async function DashboardIndexPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.monitor.dashboard" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { monitor: { dashboard: {
tabs: { rows: TabRow[] }
headerAnatomy: { items: string[] }
whereNext: { items: WhereNextItem[] }
} } }
}
const tabRows = messages.docs.monitor.dashboard.tabs.rows
const headerAnatomyItems = messages.docs.monitor.dashboard.headerAnatomy.items
const whereNextItems = messages.docs.monitor.dashboard.whereNext.items
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
const link = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/health-monitor" className="text-blue-700 hover:underline">
{chunks}
</Link>
)
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={3}
/>
<Callout variant="info" title={t("oneHeader.title")}>
{t.rich("oneHeader.body", { link })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("tabs.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("tabs.intro")}</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("tabs.headerTab")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("tabs.headerOwns")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{tabRows.map((row, idx) => (
<tr
key={row.name}
className={idx < tabRows.length - 1 ? "border-b border-gray-100" : ""}
>
<td className="px-3 py-2 align-top whitespace-nowrap">
{row.linksTo ? (
<Link href={row.linksTo} className="text-blue-600 hover:underline font-semibold">
{row.name}
</Link>
) : (
<strong>{row.name}</strong>
)}
</td>
<td className="px-3 py-2 align-top">
{t.rich(`tabs.rows.${idx}.owns`, { code })}
</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("headerAnatomy.heading")}</h2>
<ul className="list-disc pl-6 mb-6 text-gray-800 leading-relaxed space-y-1">
{headerAnatomyItems.map((_, idx) => (
<li key={idx}>{t.rich(`headerAnatomy.items.${idx}`, { code, strong, em })}</li>
))}
</ul>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("whereNext.heading")}</h2>
<ul className="list-disc pl-6 text-gray-800 leading-relaxed space-y-1">
{whereNextItems.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,489 @@
import type { Metadata } from "next"
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
import { Link } from "@/i18n/navigation"
import Image from "next/image"
import { ExternalLink } from "lucide-react"
import { DocHeader } from "@/components/ui/doc-header"
import { Callout } from "@/components/ui/callout"
import CopyableCode from "@/components/CopyableCode"
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "docs.monitor.dashboard.security.meta" })
return { title: t("title"), description: t("description") }
}
type DataRow = { card: string; endpoint: string; source: string }
type WhereNextItem = { label: string; href: string; tail?: string; tailRich?: string }
export default async function SecurityTabPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.monitor.dashboard.security" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { monitor: { dashboard: { security: {
auth: { items: string[] }
ssl: { items: string[] }
gateway: { step3Items: string[]; step4Items: string[] }
firewall: { items: string[] }
lynis: { scoreItems: string[] }
dataCollected: { rows: DataRow[] }
whereNext: { items: WhereNextItem[] }
} } } }
}
const sec = messages.docs.monitor.dashboard.security
const authItems = sec.auth.items
const sslItems = sec.ssl.items
const step3Items = sec.gateway.step3Items
const step4Items = sec.gateway.step4Items
const firewallItems = sec.firewall.items
const lynisScoreItems = sec.lynis.scoreItems
const dataRows = sec.dataCollected.rows
const whereNextItems = sec.whereNext.items
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
const authLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/access-auth" className="text-blue-600 hover:underline">{chunks}</Link>
)
const sslPageLink = (chunks: React.ReactNode) => (
<Link href="/docs/security/ssl-letsencrypt" className="text-blue-600 hover:underline">{chunks}</Link>
)
const integrationsLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/integrations" className="text-blue-600 hover:underline">{chunks}</Link>
)
const tailscaleHomeAnchor = (chunks: React.ReactNode) => (
<a href="https://tailscale.com" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline inline-flex items-center gap-1">
{chunks}
<ExternalLink className="h-3 w-3" aria-hidden="true" />
</a>
)
const tailscaleKeysAnchor = (chunks: React.ReactNode) => (
<a href="https://login.tailscale.com/admin/settings/keys" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline inline-flex items-center gap-1">
{chunks}
<ExternalLink className="h-3 w-3" aria-hidden="true" />
</a>
)
const tailscaleMachinesAnchor = (chunks: React.ReactNode) => (
<a href="https://login.tailscale.com/admin/machines" target="_blank" rel="noopener noreferrer" className="text-amber-700 hover:underline inline-flex items-center gap-1">
{chunks}
<ExternalLink className="h-3 w-3" aria-hidden="true" />
</a>
)
const fail2banLink = (chunks: React.ReactNode) => (
<Link href="/docs/security/fail2ban" className="text-blue-600 hover:underline">{chunks}</Link>
)
const lynisLink = (chunks: React.ReactNode) => (
<Link href="/docs/security/lynis" className="text-blue-600 hover:underline">{chunks}</Link>
)
const lynisSampleAnchor = (chunks: React.ReactNode) => (
<a href="/monitor/security/lynis-sample-report.pdf" target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline inline-flex items-center gap-1">
{chunks}
<ExternalLink className="h-3 w-3" aria-hidden="true" />
</a>
)
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={18}
/>
<Callout variant="info" title={t("intro.title")}>
{t.rich("intro.body", { strong })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("monitor.heading")}</h2>
<p className="mb-6 text-gray-800 leading-relaxed">{t("monitor.intro")}</p>
<h3 className="text-xl font-semibold mt-8 mb-3 text-gray-900">{t("auth.heading")}</h3>
<figure className="my-4">
<Image src="/monitor/security/auth-card.png" alt={t("auth.imageAlt")} width={2000} height={956} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t("auth.imageCaption")}</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("auth.intro", { link: authLink })}
</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{authItems.map((_, idx) => (
<li key={idx}>{t.rich(`auth.items.${idx}`, { strong, em })}</li>
))}
</ul>
<h3 className="text-xl font-semibold mt-8 mb-3 text-gray-900">{t("ssl.heading")}</h3>
<figure className="my-4">
<Image src="/monitor/security/ssl-https-card.png" alt={t("ssl.imageAlt")} width={2000} height={1124} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t.rich("ssl.imageCaption", { em })}</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("ssl.intro", { code })}
</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{sslItems.map((_, idx) => (
<li key={idx}>{t.rich(`ssl.items.${idx}`, { strong, code })}</li>
))}
</ul>
<figure className="my-4">
<Image src="/monitor/security/ssl-https-enabled.png" alt={t("ssl.enabledAlt")} width={2000} height={889} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t.rich("ssl.enabledCaption", { em })}</figcaption>
</figure>
<Callout variant="info" title={t("ssl.acmeTitle")}>
{t.rich("ssl.acmeBody", { em })}
</Callout>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("ssl.walkthroughLink", { code, link: sslPageLink })}
</p>
<h3 className="text-xl font-semibold mt-8 mb-3 text-gray-900">{t("apiTokens.heading")}</h3>
<figure className="my-4">
<Image src="/monitor/security/api-tokens-empty.png" alt={t("apiTokens.emptyAlt")} width={2000} height={855} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t.rich("apiTokens.emptyCaption", { em, code })}</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">{t("apiTokens.intro")}</p>
<p className="mb-3 text-gray-800 leading-relaxed">
{t.rich("apiTokens.generateBody", { strong, em })}
</p>
<figure className="my-4">
<Image src="/monitor/security/api-tokens-generate.png" alt={t("apiTokens.generateAlt")} width={2000} height={1124} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t("apiTokens.generateCaption")}</figcaption>
</figure>
<p className="mb-3 text-gray-800 leading-relaxed">
{t.rich("apiTokens.saveBody", { strong })}
</p>
<figure className="my-4">
<Image src="/monitor/security/api-tokens-generated.png" alt={t("apiTokens.generatedAlt")} width={2000} height={1468} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t.rich("apiTokens.generatedCaption", { code })}</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("apiTokens.outro", { em, link: integrationsLink })}
</p>
<h3 className="text-xl font-semibold mt-8 mb-3 text-gray-900">{t("gateway.heading")}</h3>
<figure className="my-4">
<Image src="/monitor/security/secure-gateway-card.png" alt={t("gateway.cardAlt")} width={2000} height={434} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t("gateway.cardCaption")}</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("gateway.intro", { code, a: tailscaleHomeAnchor })}
</p>
<h4 className="text-lg font-semibold mt-6 mb-3 text-gray-900">{t("gateway.wizardTitle")}</h4>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("gateway.wizardIntro", { em })}
</p>
<h5 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("gateway.step0Title")}</h5>
<p className="mb-3 text-gray-800 leading-relaxed">
{t.rich("gateway.step0Body", { em, a: tailscaleKeysAnchor })}
</p>
<figure className="my-4">
<Image src="/monitor/security/tailscale-auth-key-page.png" alt={t("gateway.step0Alt")} width={2000} height={1115} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t.rich("gateway.step0Caption", { em })}</figcaption>
</figure>
<h5 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("gateway.step1Title")}</h5>
<p className="mb-3 text-gray-800 leading-relaxed">
{t.rich("gateway.step1Body", { em })}
</p>
<figure className="my-4">
<Image src="/monitor/security/gateway-step-1-intro.png" alt={t("gateway.step1Alt")} width={1589} height={2000} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t("gateway.step1Caption")}</figcaption>
</figure>
<h5 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("gateway.step2Title")}</h5>
<p className="mb-3 text-gray-800 leading-relaxed">
{t.rich("gateway.step2Body", { code })}
</p>
<figure className="my-4">
<Image src="/monitor/security/gateway-step-3-auth.png" alt={t("gateway.step2Alt")} width={1985} height={2000} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t("gateway.step2Caption")}</figcaption>
</figure>
<h5 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("gateway.step3Title")}</h5>
<p className="mb-3 text-gray-800 leading-relaxed">{t("gateway.step3Intro")}</p>
<ul className="list-disc pl-6 mb-3 text-gray-800 leading-relaxed space-y-1">
{step3Items.map((_, idx) => (
<li key={idx}>{t.rich(`gateway.step3Items.${idx}`, { strong })}</li>
))}
</ul>
<figure className="my-4">
<Image src="/monitor/security/gateway-step-2-scope.png" alt={t("gateway.step3Alt")} width={1934} height={2000} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t.rich("gateway.step3Caption", { em })}</figcaption>
</figure>
<h5 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("gateway.step4Title")}</h5>
<p className="mb-3 text-gray-800 leading-relaxed">
{t.rich("gateway.step4Intro", { strong })}
</p>
<ul className="list-disc pl-6 mb-3 text-gray-800 leading-relaxed space-y-1">
{step4Items.map((_, idx) => (
<li key={idx}>{t.rich(`gateway.step4Items.${idx}`, { strong, em })}</li>
))}
</ul>
<figure className="my-4">
<Image src="/monitor/security/gateway-step-4-advanced.png" alt={t("gateway.step4Alt")} width={1847} height={2000} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t("gateway.step4Caption")}</figcaption>
</figure>
<h5 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("gateway.step5Title")}</h5>
<p className="mb-3 text-gray-800 leading-relaxed">
{t.rich("gateway.step5Body", { strong })}
</p>
<figure className="my-4">
<Image src="/monitor/security/gateway-step-5-review.png" alt={t("gateway.step5Alt")} width={1860} height={2000} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t("gateway.step5Caption")}</figcaption>
</figure>
<Callout variant="warning" title={t("gateway.approvalTitle")}>
{t.rich("gateway.approvalBody", { em, a: tailscaleMachinesAnchor })}
</Callout>
<h2 className="text-2xl font-semibold mt-12 mb-4 text-gray-900">{t("pve.heading")}</h2>
<p className="mb-6 text-gray-800 leading-relaxed">{t("pve.intro")}</p>
<h3 className="text-xl font-semibold mt-8 mb-3 text-gray-900">{t("firewall.heading")}</h3>
<figure className="my-4">
<Image src="/monitor/security/firewall-card.png" alt={t("firewall.imageAlt")} width={2000} height={1256} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t.rich("firewall.imageCaption", { em })}</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("firewall.intro", { code })}
</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{firewallItems.map((_, idx) => (
<li key={idx}>{t.rich(`firewall.items.${idx}`, { strong, em, code })}</li>
))}
</ul>
<h3 className="text-xl font-semibold mt-8 mb-3 text-gray-900">
{t("fail2ban.heading")} <em>{t("fail2ban.subHeading")}</em>
</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("fail2ban.whatIs", { strong })}
</p>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("fail2ban.notBundled", { strong })}
</p>
<figure className="my-4">
<Image src="/monitor/security/fail2ban-not-installed.png" alt={t("fail2ban.notInstalledAlt")} width={2000} height={968} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t("fail2ban.notInstalledCaption")}</figcaption>
</figure>
<p className="mb-3 text-gray-800 leading-relaxed">
{t.rich("fail2ban.clickBody", { em })}
</p>
<figure className="my-4">
<Image src="/monitor/security/fail2ban-install-confirm.png" alt={t("fail2ban.confirmAlt")} width={1808} height={1678} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t.rich("fail2ban.confirmCaption", { code })}</figcaption>
</figure>
<p className="mb-3 text-gray-800 leading-relaxed">{t("fail2ban.confirmIntro")}</p>
<figure className="my-4">
<Image src="/monitor/security/fail2ban-install-progress.png" alt={t("fail2ban.progressAlt")} width={2000} height={1512} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t("fail2ban.progressCaption")}</figcaption>
</figure>
<p className="mb-3 text-gray-800 leading-relaxed">
{t.rich("fail2ban.afterInstall", { em })}
</p>
<figure className="my-4">
<Image src="/monitor/security/fail2ban-active.png" alt={t("fail2ban.activeAlt")} width={2000} height={1614} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t.rich("fail2ban.activeCaption", { code })}</figcaption>
</figure>
<p className="mb-3 text-gray-800 leading-relaxed">
{t.rich("fail2ban.tuneBody", { strong, em })}
</p>
<figure className="my-4">
<Image src="/monitor/security/fail2ban-sshd-config.png" alt={t("fail2ban.configAlt")} width={2000} height={919} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t.rich("fail2ban.configCaption", { em })}</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("fail2ban.outro", { em, code, link: fail2banLink })}
</p>
<Callout variant="info" title={t("fail2ban.calloutTitle")}>
{t.rich("fail2ban.calloutBody", { em, code })}
</Callout>
<h3 className="text-xl font-semibold mt-8 mb-3 text-gray-900">
{t("lynis.heading")} <em>{t("lynis.subHeading")}</em>
</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("lynis.whatIs", { strong })}
</p>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("lynis.whyUseful", { strong, code })}
</p>
<figure className="my-4">
<Image src="/monitor/security/lynis-not-installed.png" alt={t("lynis.notInstalledAlt")} width={2000} height={919} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t("lynis.notInstalledCaption")}</figcaption>
</figure>
<p className="mb-3 text-gray-800 leading-relaxed">
{t.rich("lynis.notBundled", { strong })}
</p>
<figure className="my-4">
<Image src="/monitor/security/lynis-install-confirm.png" alt={t("lynis.confirmAlt")} width={1985} height={2000} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t("lynis.confirmCaption")}</figcaption>
</figure>
<figure className="my-4">
<Image src="/monitor/security/lynis-install-progress.png" alt={t("lynis.progressAlt")} width={1856} height={972} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t("lynis.progressCaption")}</figcaption>
</figure>
<p className="mb-3 text-gray-800 leading-relaxed">
{t.rich("lynis.afterInstall", { em })}
</p>
<figure className="my-4">
<Image src="/monitor/security/lynis-installed-empty.png" alt={t("lynis.installedAlt")} width={2000} height={1131} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t("lynis.installedCaption")}</figcaption>
</figure>
<figure className="my-4">
<Image src="/monitor/security/lynis-audit-running.png" alt={t("lynis.runningAlt")} width={2000} height={1131} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t("lynis.runningCaption")}</figcaption>
</figure>
<p className="mb-3 text-gray-800 leading-relaxed">
{t.rich("lynis.finishedBody", { em })}
</p>
<figure className="my-4">
<Image src="/monitor/security/lynis-audit-results.png" alt={t("lynis.resultsAlt")} width={2000} height={1183} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t.rich("lynis.resultsCaption", { strong })}</figcaption>
</figure>
<Callout variant="info" title={t("lynis.scoreTitle")}>
{t.rich("lynis.scoreIntro", { em, code })}
<ul className="list-disc pl-6 mt-2 mb-0 space-y-1">
{lynisScoreItems.map((_, idx) => (
<li key={idx}>{t.rich(`lynis.scoreItems.${idx}`, { em })}</li>
))}
</ul>
</Callout>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("lynis.reportBody", { strong, em })}
</p>
<figure className="my-4">
<Image src="/monitor/security/lynis-report-pdf.png" alt={t("lynis.reportAlt")} width={1414} height={2000} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("lynis.reportCaption", { a: lynisSampleAnchor })}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">{t("lynis.runPeriodically")}</p>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("lynis.outro", { em, link: lynisLink })}
</p>
<h2 className="text-2xl font-semibold mt-12 mb-4 text-gray-900">{t("dataCollected.heading")}</h2>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerCard")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerEndpoint")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerSource")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{dataRows.map((row, idx) => (
<tr key={row.card} className={idx < dataRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top">{row.card}</td>
<td className="px-3 py-2 align-top font-mono text-xs">{row.endpoint}</td>
<td className="px-3 py-2 align-top">{t.rich(`dataCollected.rows.${idx}.source`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<CopyableCode
code={`# Confirm the auth log on the host (used by Fail2Ban + audit)
journalctl -t proxmenux-auth --since '7 days ago' | tail
# Cross-check the firewall rules the dashboard sees
pve-firewall status
cat /etc/pve/firewall/host.fw
# Verify Fail2Ban (only if installed)
fail2ban-client status
fail2ban-client status sshd
# Verify Lynis (only if installed)
lynis show version
ls -lh /var/log/lynis-report.dat`}
className="my-4"
/>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("whereNext.heading")}</h2>
<ul className="list-disc pl-6 text-gray-800 leading-relaxed space-y-1">
{whereNextItems.map((item, idx) => (
<li key={item.href}>
<Link href={item.href} className="text-blue-600 hover:underline">
{item.label}
</Link>
{item.tailRich ? t.rich(`whereNext.items.${idx}.tailRich`, { code }) : item.tail}
</li>
))}
</ul>
</div>
)
}

View File

@@ -0,0 +1,486 @@
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"
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "docs.monitor.dashboard.settings.meta" })
return { title: t("title"), description: t("description") }
}
type ColourRow = { colour: string; range: string; meaning: string }
type ThresholdRow = { section: string; warning: string; critical: string; gates: string }
type DataRow = { card: string; endpoint: string; source: string }
type WhereNextItem = { label: string; href: string; tail?: string; tailRich?: string }
export default async function SettingsTabPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.monitor.dashboard.settings" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { monitor: { dashboard: { settings: {
health: { items: string[]; activeItems: string[] }
thresholds: {
whatForItems: string[]
colourRows: ColourRow[]
thresholdRows: ThresholdRow[]
}
lxcDetection: { whatRunsItems: string[] }
storageExclusions: { items: string[] }
interfaceExclusions: { items: string[] }
notifications: { items: string[] }
optimizations: { dotsItems: string[] }
dataCollected: { rows: DataRow[] }
whereNext: { items: WhereNextItem[] }
} } } }
}
const s = messages.docs.monitor.dashboard.settings
const healthItems = s.health.items
const activeSuppressionItems = s.health.activeItems
const whatForItems = s.thresholds.whatForItems
const colourRows = s.thresholds.colourRows
const thresholdRows = s.thresholds.thresholdRows
const whatRunsItems = s.lxcDetection.whatRunsItems
const storageItems = s.storageExclusions.items
const interfaceItems = s.interfaceExclusions.items
const notificationItems = s.notifications.items
const dotsItems = s.optimizations.dotsItems
const dataRows = s.dataCollected.rows
const whereNextItems = s.whereNext.items
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
const green = () => <span className="inline-block w-2 h-2 rounded-full bg-green-500 align-middle mr-1" />
const amber = () => <span className="inline-block w-2 h-2 rounded-full bg-amber-500 align-middle mr-1" />
const healthLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/health-monitor#dismissing-alerts-and-the-suppression-duration" className="text-blue-700 hover:underline">{chunks}</Link>
)
const storageTabLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/dashboard/storage" className="text-blue-600 hover:underline">{chunks}</Link>
)
const networkTabLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/dashboard/network" className="text-blue-600 hover:underline">{chunks}</Link>
)
const notifLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/notifications" className="text-blue-600 hover:underline">{chunks}</Link>
)
const aiLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/ai-assistant" className="text-blue-600 hover:underline">{chunks}</Link>
)
const autoLink = (chunks: React.ReactNode) => (
<Link href="/docs/post-install/automated" className="text-blue-600 hover:underline">{chunks}</Link>
)
const customLink = (chunks: React.ReactNode) => (
<Link href="/docs/post-install/customizable" className="text-blue-600 hover:underline">{chunks}</Link>
)
const updatesLink = (chunks: React.ReactNode) => (
<Link href="/docs/post-install/updates" className="text-blue-600 hover:underline">{chunks}</Link>
)
const uninstallLink = (chunks: React.ReactNode) => (
<Link href="/docs/post-install/uninstall" className="text-blue-600 hover:underline">{chunks}</Link>
)
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={9}
/>
<Callout variant="info" title={t("intro.title")}>
{t("intro.body")}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("networkUnits.heading")}</h2>
<figure className="my-4">
<Image
src="/monitor/settings/network-units.png"
alt={t("networkUnits.imageAlt")}
width={2000}
height={374}
className="rounded-lg border border-gray-200 shadow-sm w-full h-auto"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("networkUnits.imageCaption")}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("networkUnits.body", { strong })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("health.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("health.intro", { strong })}
</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{healthItems.map((_, idx) => (
<li key={idx}>{t.rich(`health.items.${idx}`, { strong })}</li>
))}
</ul>
<figure className="my-6">
<Image
src="/monitor/health-suppression-settings.png"
alt={t("health.imageAlt")}
width={2010}
height={1816}
className="rounded-lg border border-gray-200 shadow-sm w-full h-auto"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("health.imageCaption")}
</figcaption>
</figure>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("health.editTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("health.editBody", { strong })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("health.activeTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("health.activeIntro", { strong, em })}
</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{activeSuppressionItems.map((_, idx) => (
<li key={idx}>{t.rich(`health.activeItems.${idx}`, { strong, em, code })}</li>
))}
</ul>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("health.activeReenableTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("health.activeReenableBody", { strong, code })}
</p>
<Callout variant="info" title={t("health.activeAutoRefreshTitle")}>
{t("health.activeAutoRefreshBody")}
</Callout>
<p className="mt-4 mb-4 text-gray-800 leading-relaxed">
{t.rich("health.activePermanentNote", { strong, em, code })}
</p>
<Callout variant="info" title={t("health.calloutTitle")}>
{t.rich("health.calloutBody", { link: healthLink })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("thresholds.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("thresholds.intro", { em, strong })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">{t("thresholds.whatForTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">{t("thresholds.whatForIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{whatForItems.map((_, idx) => (
<li key={idx}>{t(`thresholds.whatForItems.${idx}`)}</li>
))}
</ul>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("thresholds.whatForOutro", { strong, code })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900" id="status-colours">{t("thresholds.coloursTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">{t("thresholds.coloursIntro")}</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("thresholds.headerColour")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("thresholds.headerRange")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("thresholds.headerMeaning")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{colourRows.map((row, idx) => {
// Color tier is identified positionally so the dot stays
// correct in any locale (Spanish: Verde / Ámbar / Rojo).
const dotClass = ["bg-green-500", "bg-amber-500", "bg-red-500"][idx] ?? "bg-red-500"
return (
<tr key={row.colour} className={idx < colourRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap">
<span className={`inline-block w-3 h-3 rounded-full ${dotClass} align-middle mr-2`} />
<strong>{row.colour}</strong>
</td>
<td className="px-3 py-2 align-top">{row.range}</td>
<td className="px-3 py-2 align-top">{row.meaning}</td>
</tr>
)
})}
</tbody>
</table>
</div>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">{t("thresholds.sectionsTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("thresholds.sectionsIntro", { em })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("thresholds.headerSection")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap bg-amber-100/60">{t("thresholds.headerWarning")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200 whitespace-nowrap bg-red-100/60">{t("thresholds.headerCritical")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("thresholds.headerGates")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{thresholdRows.map((row, idx) => (
<tr key={row.section} className={idx < thresholdRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top"><strong>{row.section}</strong></td>
<td className={`px-3 py-2 align-top font-mono whitespace-nowrap ${row.warning === "—" ? "text-gray-400" : ""} bg-amber-100/40`}>{row.warning}</td>
<td className="px-3 py-2 align-top font-mono whitespace-nowrap bg-red-100/40">{row.critical}</td>
<td className="px-3 py-2 align-top">{t.rich(`thresholds.thresholdRows.${idx}.gates`, { code, em })}</td>
</tr>
))}
</tbody>
</table>
</div>
<Callout variant="info" title={t("thresholds.defaultsTitle")}>
{t.rich("thresholds.defaultsBody", { em, strong })}
</Callout>
<Callout variant="tip" title={t("thresholds.validationTitle")}>
{t("thresholds.validationBody")}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("lxcDetection.heading")}</h2>
<figure className="my-4">
<Image
src="/monitor/settings/lxc-update-detection.png"
alt={t("lxcDetection.imageAlt")}
width={2000}
height={620}
className="rounded-lg border border-gray-200 shadow-sm w-full h-auto"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("lxcDetection.imageCaption", { code })}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("lxcDetection.intro", { code })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">{t("lxcDetection.whatRunsTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("lxcDetection.whatRunsIntro", { code })}
</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{whatRunsItems.map((_, idx) => (
<li key={idx}>{t.rich(`lxcDetection.whatRunsItems.${idx}`, { strong, code })}</li>
))}
</ul>
<Callout variant="info" title={t("lxcDetection.selfUpdateTitle")}>
{t.rich("lxcDetection.selfUpdateBody", { code })}
</Callout>
<Callout variant="tip" title={t("lxcDetection.refreshTitle")}>
{t.rich("lxcDetection.refreshBody", { code })}
</Callout>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">{t("lxcDetection.toggleTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("lxcDetection.toggleBody", { code, strong })}
</p>
<Callout variant="warning" title={t("lxcDetection.purgeTitle")}>
{t.rich("lxcDetection.purgeBody", { code })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("storageExclusions.heading")}</h2>
<figure className="my-4">
<Image
src="/monitor/settings/storage-exclusions.png"
alt={t("storageExclusions.imageAlt")}
width={2000}
height={1120}
className="rounded-lg border border-gray-200 shadow-sm w-full h-auto"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("storageExclusions.imageCaption", { em })}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">{t("storageExclusions.intro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{storageItems.map((_, idx) => (
<li key={idx}>{t.rich(`storageExclusions.items.${idx}`, { strong })}</li>
))}
</ul>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("storageExclusions.outro", { em, code, link: storageTabLink })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("interfaceExclusions.heading")}</h2>
<figure className="my-4">
<Image
src="/monitor/settings/interface-exclusions.png"
alt={t("interfaceExclusions.imageAlt")}
width={2000}
height={1142}
className="rounded-lg border border-gray-200 shadow-sm w-full h-auto"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("interfaceExclusions.imageCaption", { em })}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">{t("interfaceExclusions.intro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{interfaceItems.map((_, idx) => (
<li key={idx}>{t.rich(`interfaceExclusions.items.${idx}`, { code })}</li>
))}
</ul>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("interfaceExclusions.outro", { code, em, link: networkTabLink })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("notifications.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("notifications.body1", { em })}
</p>
<p className="mb-4 text-gray-800 leading-relaxed">{t("notifications.body2")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{notificationItems.map((_, idx) => (
<li key={idx}>{t.rich(`notifications.items.${idx}`, { notifLink, aiLink })}</li>
))}
</ul>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("optimizations.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("optimizations.intro", { code, autoLink, customLink })}
</p>
<figure className="my-4">
<Image
src="/monitor/settings/proxmenux-optimizations.png"
alt={t("optimizations.imageAlt")}
width={2000}
height={1146}
className="rounded-lg border border-gray-200 shadow-sm w-full h-auto"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("optimizations.imageCaption", { em })}
</figcaption>
</figure>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">{t("optimizations.dotsTitle")}</h3>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{dotsItems.map((_, idx) => (
<li key={idx}>{t.rich(`optimizations.dotsItems.${idx}`, { strong, em, green, amber })}</li>
))}
</ul>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">{t("optimizations.clickTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("optimizations.clickBody", { code })}
</p>
<figure className="my-4">
<Image
src="/monitor/settings/optimization-detail.png"
alt={t("optimizations.detailAlt")}
width={2000}
height={1040}
className="rounded-lg border border-gray-200 shadow-sm w-full h-auto"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("optimizations.detailCaption", { em, code })}
</figcaption>
</figure>
<Callout variant="info" title={t("optimizations.whyTitle")}>
{t("optimizations.whyBody")}
</Callout>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">{t("optimizations.updatesTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("optimizations.updatesBody", { strong, em })}
</p>
<figure className="my-4">
<Image
src="/monitor/settings/proxmenux-optimizations-update-banner.png"
alt={t("optimizations.updatesAlt")}
width={2000}
height={1146}
className="rounded-lg border border-gray-200 shadow-sm w-full h-auto"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("optimizations.updatesCaption", { link: updatesLink })}
</figcaption>
</figure>
<h3 className="text-lg font-semibold mt-6 mb-3 text-gray-900">{t("optimizations.revertTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("optimizations.revertBody", { code, link: uninstallLink })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("dataCollected.heading")}</h2>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerCard")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerEndpoint")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerSource")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{dataRows.map((row, idx) => (
<tr key={row.card} className={idx < dataRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top">{row.card}</td>
<td className="px-3 py-2 align-top font-mono text-xs">{row.endpoint}</td>
<td className="px-3 py-2 align-top">{t.rich(`dataCollected.rows.${idx}.source`, { code, notifLink, aiLink })}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("whereNext.heading")}</h2>
<ul className="list-disc pl-6 text-gray-800 leading-relaxed space-y-1">
{whereNextItems.map((item, idx) => (
<li key={item.href}>
<Link href={item.href} className="text-blue-600 hover:underline">
{item.label}
</Link>
{item.tailRich ? t.rich(`whereNext.items.${idx}.tailRich`, { customLink }) : item.tail}
</li>
))}
</ul>
</div>
)
}

View File

@@ -0,0 +1,474 @@
import type { Metadata } from "next"
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
import { Link } from "@/i18n/navigation"
import { Download } from "lucide-react"
import { DocHeader } from "@/components/ui/doc-header"
import { Callout } from "@/components/ui/callout"
import CopyableCode from "@/components/CopyableCode"
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "docs.monitor.dashboard.storage.meta" })
return { title: t("title"), description: t("description") }
}
type DataRow = { section: string; endpoint: string; source: string }
type WhereNextItem = { label: string; href: string; tail?: string; tailRich?: string }
export default async function StorageTabPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.monitor.dashboard.storage" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { monitor: { dashboard: { storage: {
thresholds: { items: string[] }
topRow: { disksItems: string[] }
pveStorage: { items: string[] }
zfs: { items: string[] }
physical: { items: string[] }
drillIn: {
overviewItems: string[]
smartItems: string[]
pdfSections: string[]
historyItems: string[]
scheduleItems: string[]
tempShowsItems: string[]
tempDiskTypes: string[]
tempWhyItems: string[]
obsWhatItems: string[]
obsWhyItems: string[]
}
dataCollected: { rows: DataRow[] }
whereNext: { items: WhereNextItem[] }
} } } }
}
const s = messages.docs.monitor.dashboard.storage
const thresholdsItems = s.thresholds.items
const disksItems = s.topRow.disksItems
const pveItems = s.pveStorage.items
const zfsItems = s.zfs.items
const physicalItems = s.physical.items
const overviewItems = s.drillIn.overviewItems
const smartItems = s.drillIn.smartItems
const pdfSections = s.drillIn.pdfSections
const historyItems = s.drillIn.historyItems
const scheduleItems = s.drillIn.scheduleItems
const tempShowsItems = s.drillIn.tempShowsItems
const tempDiskTypes = s.drillIn.tempDiskTypes
const tempWhyItems = s.drillIn.tempWhyItems
const obsWhatItems = s.drillIn.obsWhatItems
const obsWhyItems = s.drillIn.obsWhyItems
const dataRows = s.dataCollected.rows
const whereNextItems = s.whereNext.items
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
const green = () => <span className="inline-block w-2.5 h-2.5 rounded-full bg-green-500 align-middle mr-1" />
const amber = () => <span className="inline-block w-2.5 h-2.5 rounded-full bg-amber-500 align-middle mr-1" />
const red = () => <span className="inline-block w-2.5 h-2.5 rounded-full bg-red-500 align-middle mr-1" />
const thresholdsLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/dashboard/settings#status-colours" className="text-blue-700 hover:underline">{chunks}</Link>
)
const zfsHmLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/health-monitor" className="text-blue-600 hover:underline">{chunks}</Link>
)
const physicalWarnLink = (chunks: React.ReactNode) => (
<Link href="/docs/disk-manager/format-disk" className="text-amber-700 hover:underline">{chunks}</Link>
)
const hmLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/health-monitor" className="text-blue-600 hover:underline">{chunks}</Link>
)
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={14}
/>
<Callout variant="info" title={t("intro.title")}>
{t.rich("intro.body", { code })}
</Callout>
<Callout variant="tip" title={t("thresholds.title")}>
{t.rich("thresholds.intro", { strong, green, amber, red })}
<ul className="list-disc pl-6 mt-2 space-y-0.5">
{thresholdsItems.map((_, idx) => (
<li key={idx}>{t.rich(`thresholds.items.${idx}`, { strong })}</li>
))}
</ul>
{t.rich("thresholds.outro", { link: thresholdsLink })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("topRow.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("topRow.intro")}</p>
<figure className="my-6">
<img
src="/monitor/storage-top-row.png"
alt={t("topRow.imageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("topRow.imageCaption")}
</figcaption>
</figure>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("topRow.headerCard")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("topRow.headerWhat")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
<tr className="border-b border-gray-100">
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{t("topRow.totalLabel")}</strong></td>
<td className="px-3 py-2 align-top">{t("topRow.totalWhat")}</td>
</tr>
<tr className="border-b border-gray-100">
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{t("topRow.localLabel")}</strong></td>
<td className="px-3 py-2 align-top">{t.rich("topRow.localWhat", { em })}</td>
</tr>
<tr className="border-b border-gray-100">
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{t("topRow.remoteLabel")}</strong></td>
<td className="px-3 py-2 align-top">{t("topRow.remoteWhat")}</td>
</tr>
<tr>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{t("topRow.disksLabel")}</strong></td>
<td className="px-3 py-2 align-top">
{t("topRow.disksIntro")}
<ul className="list-disc pl-5 mt-2 space-y-0.5">
{disksItems.map((_, idx) => (
<li key={idx}>{t.rich(`topRow.disksItems.${idx}`, { strong, em })}</li>
))}
</ul>
</td>
</tr>
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("pveStorage.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("pveStorage.intro", { code })}
</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{pveItems.map((_, idx) => (
<li key={idx}>{t.rich(`pveStorage.items.${idx}`, { strong })}</li>
))}
</ul>
<Callout variant="tip" title={t("pveStorage.calloutTitle")}>
{t.rich("pveStorage.calloutBody", { em, code })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("zfs.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("zfs.intro")}</p>
<ul className="list-disc pl-6 mb-6 text-gray-800 leading-relaxed space-y-1">
{zfsItems.map((_, idx) => (
<li key={idx}>{t.rich(`zfs.items.${idx}`, { strong })}</li>
))}
</ul>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("zfs.outro", { em, link: zfsHmLink })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("physical.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("physical.intro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{physicalItems.map((_, idx) => (
<li key={idx}>{t.rich(`physical.items.${idx}`, { strong, em, code })}</li>
))}
</ul>
<p className="mb-6 text-gray-800 leading-relaxed">{t("physical.clickHint")}</p>
<Callout variant="warning" title={t("physical.warningTitle")}>
{t.rich("physical.warningBody", { strong, link: physicalWarnLink })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("external.heading")}</h2>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("external.body", { strong })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("drillIn.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("drillIn.intro", { strong, em })}
</p>
<h3 className="text-lg font-semibold mt-8 mb-2 text-gray-900">{t("drillIn.overviewTitle")}</h3>
<figure className="my-4">
<img
src="/monitor/disk-modal-overview.png"
alt={t("drillIn.overviewImageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("drillIn.overviewImageCaption")}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">{t("drillIn.overviewIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{overviewItems.map((_, idx) => (
<li key={idx}>{t.rich(`drillIn.overviewItems.${idx}`, { strong, em })}</li>
))}
</ul>
<h3 className="text-lg font-semibold mt-8 mb-2 text-gray-900">{t("drillIn.smartTitle")}</h3>
<figure className="my-4">
<img
src="/monitor/disk-modal-smart.png"
alt={t("drillIn.smartImageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("drillIn.smartImageCaption")}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">{t("drillIn.smartIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{smartItems.map((_, idx) => (
<li key={idx}>{t.rich(`drillIn.smartItems.${idx}`, { strong, em, code })}</li>
))}
</ul>
<h4 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("drillIn.pdfTitle")}</h4>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("drillIn.pdfIntro", { strong })}
</p>
<figure className="my-6">
<img
src="/monitor/smart-report-preview.png"
alt={t("drillIn.pdfPreviewAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("drillIn.pdfPreviewCaption")}
</figcaption>
</figure>
<div className="my-6">
<a
href="/monitor/sample-smart-report.pdf"
download
className="inline-flex items-center gap-2 px-4 py-2 rounded-md border border-blue-200 bg-blue-50 text-blue-700 font-medium hover:bg-blue-100 transition-colors"
>
<Download className="h-4 w-4" aria-hidden="true" />
{t("drillIn.pdfDownloadLabel")}
</a>
</div>
<p className="mb-4 text-gray-800 leading-relaxed">{t("drillIn.pdfSectionsIntro")}</p>
<ol className="list-decimal pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{pdfSections.map((_, idx) => (
<li key={idx}>{t.rich(`drillIn.pdfSections.${idx}`, { strong, em })}</li>
))}
</ol>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("drillIn.pdfOutro", { code })}
</p>
<h3 className="text-lg font-semibold mt-8 mb-2 text-gray-900">{t("drillIn.historyTitle")}</h3>
<figure className="my-4">
<img
src="/monitor/disk-modal-history.png"
alt={t("drillIn.historyImageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("drillIn.historyImageCaption", { code })}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("drillIn.historyIntro", { code })}
</p>
<ul className="list-disc pl-6 mb-6 text-gray-800 leading-relaxed space-y-1">
{historyItems.map((_, idx) => (
<li key={idx}>{t.rich(`drillIn.historyItems.${idx}`, { strong, em, code })}</li>
))}
</ul>
<h3 className="text-lg font-semibold mt-8 mb-2 text-gray-900">{t("drillIn.scheduleTitle")}</h3>
<figure className="my-4">
<img
src="/monitor/disk-modal-schedule.png"
alt={t("drillIn.scheduleImageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("drillIn.scheduleImageCaption", { code })}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">{t("drillIn.scheduleIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{scheduleItems.map((_, idx) => (
<li key={idx}>{t.rich(`drillIn.scheduleItems.${idx}`, { strong, em })}</li>
))}
</ul>
<p className="mb-6 text-gray-800 leading-relaxed">{t("drillIn.scheduleOutro")}</p>
<h3 className="text-lg font-semibold mt-8 mb-2 text-gray-900">{t("drillIn.tempTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">{t("drillIn.tempIntro")}</p>
<figure className="my-4">
<img
src="/monitor/disk-modal-temperature.png"
alt={t("drillIn.tempImageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("drillIn.tempImageCaption")}
</figcaption>
</figure>
<h4 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("drillIn.tempShowsTitle")}</h4>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{tempShowsItems.map((_, idx) => (
<li key={idx}>
{t.rich(`drillIn.tempShowsItems.${idx}`, { strong, em })}
{idx === 2 && (
<ul className="list-disc pl-6 mt-1">
{tempDiskTypes.map((_, didx) => (
<li key={didx}>{t.rich(`drillIn.tempDiskTypes.${didx}`, { strong })}</li>
))}
</ul>
)}
</li>
))}
</ul>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("drillIn.tempConfigurable", { em })}
</p>
<h4 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("drillIn.tempWhyTitle")}</h4>
<ul className="list-disc pl-6 mb-6 text-gray-800 leading-relaxed space-y-1">
{tempWhyItems.map((_, idx) => (
<li key={idx}>{t.rich(`drillIn.tempWhyItems.${idx}`, { strong, em })}</li>
))}
</ul>
<h3 className="text-lg font-semibold mt-8 mb-2 text-gray-900">{t("drillIn.obsTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("drillIn.obsIntro", { strong, em })}
</p>
<figure className="my-4">
<img
src="/monitor/disk-modal-observations.png"
alt={t("drillIn.obsImageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("drillIn.obsImageCaption", { strong })}
</figcaption>
</figure>
<h4 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("drillIn.obsWhatTitle")}</h4>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("drillIn.obsWhatIntro", { strong })}
</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{obsWhatItems.map((_, idx) => (
<li key={idx}>{t.rich(`drillIn.obsWhatItems.${idx}`, { strong, code })}</li>
))}
</ul>
<h4 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("drillIn.obsWhyTitle")}</h4>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{obsWhyItems.map((_, idx) => (
<li key={idx}>{t.rich(`drillIn.obsWhyItems.${idx}`, { strong, em })}</li>
))}
</ul>
<h4 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("drillIn.obsDedupTitle")}</h4>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("drillIn.obsDedupBody1", { strong, code })}
</p>
<p className="mb-4 text-gray-800 leading-relaxed">
{t("drillIn.obsDedupBody2")}
</p>
<h4 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("drillIn.obsDismissTitle")}</h4>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("drillIn.obsDismissBody1", { strong })}
</p>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("drillIn.obsDismissBody2", { link: hmLink })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("dataCollected.heading")}</h2>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerSection")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerEndpoint")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerSource")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{dataRows.map((row, idx) => (
<tr key={row.section} className={idx < dataRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top">{row.section}</td>
<td className="px-3 py-2 align-top font-mono text-xs">{row.endpoint}</td>
<td className="px-3 py-2 align-top">{t.rich(`dataCollected.rows.${idx}.source`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mb-4 text-gray-800 leading-relaxed">{t("dataCollected.outro")}</p>
<CopyableCode
code={`${t("dataCollected.codeComment1")}
curl -H "Authorization: Bearer <api-token>" \\
http://<host>:8008/api/storage | jq '.disks[] | {name,model,smart_status}'
${t("dataCollected.codeComment2")}
lsblk -O
zpool status
journalctl -t smartd --since '1 day ago' | tail`}
className="my-4"
/>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("whereNext.heading")}</h2>
<ul className="list-disc pl-6 text-gray-800 leading-relaxed space-y-1">
{whereNextItems.map((item, idx) => (
<li key={item.href}>
<Link href={item.href} className="text-blue-600 hover:underline">
{item.label}
</Link>
{item.tailRich ? t.rich(`whereNext.items.${idx}.tailRich`, { code }) : item.tail}
</li>
))}
</ul>
</div>
)
}

View File

@@ -0,0 +1,178 @@
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.monitor.dashboard.systemLogs.meta" })
return { title: t("title"), description: t("description") }
}
type DataRow = { subtab: string; endpoint: string; source: string }
type WhereNextItem = { label: string; href: string; tail: string }
export default async function SystemLogsTabPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.monitor.dashboard.systemLogs" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { monitor: { dashboard: { systemLogs: {
topRow: { items: string[] }
subtabs: { logsFilters: string[]; fields: string[] }
dataCollected: { rows: DataRow[] }
whereNext: { items: WhereNextItem[] }
} } } }
}
const sl = messages.docs.monitor.dashboard.systemLogs
const topRowItems = sl.topRow.items
const logsFilters = sl.subtabs.logsFilters
const fields = sl.subtabs.fields
const dataRows = sl.dataCollected.rows
const whereNextItems = sl.whereNext.items
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
const link = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/notifications" className="text-blue-600 hover:underline">
{chunks}
</Link>
)
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={7}
/>
<Callout variant="info" title={t("readOnly.title")}>
{t.rich("readOnly.body", { code })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("topRow.heading")}</h2>
<ul className="list-disc pl-6 mb-6 text-gray-800 leading-relaxed space-y-1">
{topRowItems.map((_, idx) => (
<li key={idx}>{t.rich(`topRow.items.${idx}`, { code, strong })}</li>
))}
</ul>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("subtabs.heading")}</h2>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("subtabs.logsTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("subtabs.logsIntro", { code })}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{logsFilters.map((_, idx) => (
<li key={idx}>{t.rich(`subtabs.logsFilters.${idx}`, { code, strong })}</li>
))}
</ul>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("subtabs.logsRowsAfter", { code, strong })}
</p>
<h4 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("subtabs.logDetailsModalTitle")}</h4>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("subtabs.logDetailsBody", { code, strong })}
</p>
<figure className="my-6">
<img
src="/monitor/log-details-modal.png"
alt={t("subtabs.logDetailsImageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("subtabs.logDetailsImageCaption")}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">{t("subtabs.fieldsIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{fields.map((_, idx) => (
<li key={idx}>{t.rich(`subtabs.fields.${idx}`, { code, strong })}</li>
))}
</ul>
<Callout variant="tip" title={t("subtabs.maxLevelStoreTitle")}>
{t.rich("subtabs.maxLevelStoreBody", { code })}
</Callout>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("subtabs.backupsTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">{t.rich("subtabs.backupsBody", { code, em })}</p>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("subtabs.notificationsTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">{t("subtabs.notificationsBody1")}</p>
<p className="mb-6 text-gray-800 leading-relaxed">{t.rich("subtabs.notificationsBody2", { link })}</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("dataCollected.heading")}</h2>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerSubtab")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerEndpoint")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerSource")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{dataRows.map((row, idx) => (
<tr
key={row.endpoint}
className={idx < dataRows.length - 1 ? "border-b border-gray-100" : ""}
>
<td className="px-3 py-2 align-top">{row.subtab}</td>
<td className="px-3 py-2 align-top font-mono text-xs" dangerouslySetInnerHTML={{ __html: row.endpoint }} />
<td className="px-3 py-2 align-top">
{t.rich(`dataCollected.rows.${idx}.source`, { code })}
</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mb-4 text-gray-800 leading-relaxed">{t("dataCollected.apiIntro")}</p>
<CopyableCode
code={`${t("dataCollected.codeComment1")}
curl -H "Authorization: Bearer <token>" \\
"http://<host>:8008/api/logs?severity=error&since=1h&search=zfs"
${t("dataCollected.codeComment2")}
curl -H "Authorization: Bearer <token>" \\
-o pmx-journal.txt \\
"http://<host>:8008/api/logs/download?since=6h"
${t("dataCollected.codeComment3")}
curl -H "Authorization: Bearer <token>" \\
"http://<host>:8008/api/task-log/<upid>"`}
className="my-4"
/>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("whereNext.heading")}</h2>
<ul className="list-disc pl-6 text-gray-800 leading-relaxed space-y-1">
{whereNextItems.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,225 @@
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.monitor.dashboard.systemOverview.meta" })
return { title: t("title"), description: t("description") }
}
type TopRow = { card: string; what: string; source: string }
type DataRow = { card: string; endpoint: string; source: string }
type WhereNextItem = { label: string; href: string; tail: string }
export default async function SystemOverviewTabPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.monitor.dashboard.systemOverview" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { monitor: { dashboard: { systemOverview: {
topRow: { rows: TopRow[]; thresholdsItems: string[] }
bottom: { storageItems: string[] }
refresh: { items: string[] }
dataCollected: { rows: DataRow[] }
whereNext: { items: WhereNextItem[] }
} } } }
}
const so = messages.docs.monitor.dashboard.systemOverview
const topRows = so.topRow.rows
const thresholdsItems = so.topRow.thresholdsItems
const storageItems = so.bottom.storageItems
const refreshItems = so.refresh.items
const dataRows = so.dataCollected.rows
const whereNextItems = so.whereNext.items
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
const green = () => <span className="inline-block w-2.5 h-2.5 rounded-full bg-green-500 align-middle mr-1" />
const amber = () => <span className="inline-block w-2.5 h-2.5 rounded-full bg-amber-500 align-middle mr-1" />
const red = () => <span className="inline-block w-2.5 h-2.5 rounded-full bg-red-500 align-middle mr-1" />
const thresholdsLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/dashboard/settings#status-colours" className="text-blue-700 hover:underline">
{chunks}
</Link>
)
const storageLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/dashboard" className="text-blue-600 hover:underline">
{chunks}
</Link>
)
const networkLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/dashboard" className="text-blue-600 hover:underline">
{chunks}
</Link>
)
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={6}
/>
<Callout variant="info" title={t("readOnly.title")}>
{t("readOnly.body")}
</Callout>
<figure className="my-8">
<img
src="/monitor/dashboard-home.png"
alt={t("captureAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("captureCaption")}
</figcaption>
</figure>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("topRow.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("topRow.intro", { code })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("topRow.headerCard")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("topRow.headerWhat")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("topRow.headerSource")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{topRows.map((row, idx) => (
<tr key={row.card} className={idx < topRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap">
<strong>{row.card}</strong>
</td>
<td className="px-3 py-2 align-top">
{t.rich(`topRow.rows.${idx}.what`, { code, em })}
</td>
<td className="px-3 py-2 align-top font-mono text-xs">{row.source}</td>
</tr>
))}
</tbody>
</table>
</div>
<Callout variant="tip" title={t("topRow.thresholdsTitle")}>
{t.rich("topRow.thresholdsIntro", { strong, green, amber, red })}
<ul className="list-disc pl-6 mt-2 space-y-0.5">
{thresholdsItems.map((_, idx) => (
<li key={idx}>{t.rich(`topRow.thresholdsItems.${idx}`, { strong })}</li>
))}
</ul>
{t.rich("topRow.thresholdsOutro", { link: thresholdsLink })}
</Callout>
<Callout variant="tip" title={t("topRow.sparklineTitle")}>
{t("topRow.sparklineBody")}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("middle.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("middle.body1", { code, em })}
</p>
<p className="mb-6 text-gray-800 leading-relaxed">
{t("middle.body2")}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("bottom.heading")}</h2>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("bottom.storageTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">{t("bottom.storageIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{storageItems.map((_, idx) => (
<li key={idx}>{t.rich(`bottom.storageItems.${idx}`, { strong })}</li>
))}
</ul>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("bottom.storageDrillIn", { link: storageLink })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("bottom.networkTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("bottom.networkBody1", { code })}
</p>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("bottom.networkBody2", { link: networkLink })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("refresh.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("refresh.intro", { code, em })}
</p>
<ul className="list-disc pl-6 mb-6 text-gray-800 leading-relaxed space-y-1">
{refreshItems.map((_, idx) => (
<li key={idx}>{t.rich(`refresh.items.${idx}`, { strong })}</li>
))}
</ul>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("dataCollected.heading")}</h2>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerCard")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerEndpoint")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerSource")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{dataRows.map((row, idx) => (
<tr key={row.card} className={idx < dataRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top">{row.card}</td>
<td className="px-3 py-2 align-top font-mono text-xs">{row.endpoint}</td>
<td className="px-3 py-2 align-top">
{t.rich(`dataCollected.rows.${idx}.source`, { code })}
</td>
</tr>
))}
</tbody>
</table>
</div>
<CopyableCode
code={`${t("dataCollected.codeComment1")}
curl http://<host>:8008/api/health ${t("dataCollected.codeComment2")}
${t("dataCollected.codeComment3")}
curl -H "Authorization: Bearer <token>" \\
http://<host>:8008/api/system | jq '.cpu,.memory,.uptime'`}
className="my-4"
/>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("whereNext.heading")}</h2>
<ul className="list-disc pl-6 text-gray-800 leading-relaxed space-y-1">
{whereNextItems.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,319 @@
import type { Metadata } from "next"
import { getTranslations, getMessages, setRequestLocale } from "next-intl/server"
import { Link } from "@/i18n/navigation"
import Image from "next/image"
import { ExternalLink } 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.monitor.dashboard.terminal.meta" })
return { title: t("title"), description: t("description") }
}
type KeyboardRow = { button: string; sends: string; use?: string; useRich?: boolean }
type DisconnectRow = { cause: string; fix: string }
type WhereNextItem = { label: string; href: string; tail: string }
export default async function TerminalTabPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.monitor.dashboard.terminal" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { monitor: { dashboard: { terminal: {
keyboard: { rows: KeyboardRow[]; ctrlItems: string[] }
auth: { items: string[] }
clipboard: { items: string[] }
disconnect: { rows: DisconnectRow[] }
fourTerminals: { items: string[] }
whereNext: { items: WhereNextItem[] }
} } } }
}
const term = messages.docs.monitor.dashboard.terminal
const kbRows = term.keyboard.rows
const ctrlItems = term.keyboard.ctrlItems
const authItems = term.auth.items
const clipboardItems = term.clipboard.items
const disconnectRows = term.disconnect.rows
const fourTerminalsItems = term.fourTerminals.items
const whereNextItems = term.whereNext.items
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
const green = (chunks: React.ReactNode) => <span className="text-green-600 font-semibold">{chunks}</span>
const red = (chunks: React.ReactNode) => <span className="text-red-600 font-semibold">{chunks}</span>
const vmsLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/dashboard/vms-lxcs" className="text-blue-600 hover:underline">
{chunks}
</Link>
)
const vmsLinkAmber = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/dashboard/vms-lxcs" className="text-blue-700 hover:underline">
{chunks}
</Link>
)
const authLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/access-auth" className="text-blue-600 hover:underline">
{chunks}
</Link>
)
const authLinkWarn = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/access-auth" className="text-amber-700 hover:underline">
{chunks}
</Link>
)
const gatewayLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/integrations" className="text-amber-700 hover:underline">
{chunks}
</Link>
)
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={7}
/>
<Callout variant="info" title={t("intro.title")}>
{t.rich("intro.body", { code })}
</Callout>
<figure className="my-6">
<Image
src="/monitor/terminal/single-terminal.png"
alt={t("singleAlt")}
width={1600}
height={1000}
className="rounded-lg border border-gray-200 shadow-sm w-full h-auto"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("singleCaption", { em })}
</figcaption>
</figure>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("target.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("target.body1", { strong })}
</p>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("target.body2", { strong, em, code, link: vmsLink })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("fourTerminals.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("fourTerminals.intro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{fourTerminalsItems.map((_, idx) => (
<li key={idx}>{t.rich(`fourTerminals.items.${idx}`, { strong, em, code })}</li>
))}
</ul>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("fourTerminals.outro", { strong, em, code })}
</p>
<figure className="my-6">
<Image
src="/monitor/terminal/grid-4-terminals.png"
alt={t("gridAlt")}
width={1600}
height={1000}
className="rounded-lg border border-gray-200 shadow-sm w-full h-auto"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("gridCaption", { code })}
</figcaption>
</figure>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("keyboard.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("keyboard.intro", { code })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("keyboard.headerButton")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("keyboard.headerSends")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("keyboard.headerUse")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{kbRows.map((row, idx) => (
<tr key={row.button} className={idx < kbRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{row.button}</strong></td>
<td className="px-3 py-2 align-top font-mono text-xs">{row.sends}</td>
<td className="px-3 py-2 align-top">
{row.useRich ? (
<>
{t("keyboard.ctrlIntro")}
<ul className="list-disc pl-5 mt-1 space-y-0.5">
{ctrlItems.map((_, cidx) => (
<li key={cidx}>{t.rich(`keyboard.ctrlItems.${cidx}`, { code })}</li>
))}
</ul>
</>
) : (
t.rich(`keyboard.rows.${idx}.use`, { code })
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
<Callout variant="info" title={t("keyboard.modalTitle")}>
{t.rich("keyboard.modalBody", { code, link: vmsLinkAmber })}
</Callout>
<figure className="my-6">
<Image
src="/monitor/terminal/lxc-console-modal.png"
alt={t("lxcAlt")}
width={1600}
height={1200}
className="rounded-lg border border-gray-200 shadow-sm w-full h-auto"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("lxcCaption", { em })}
</figcaption>
</figure>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("search.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("search.intro", { code, strong, em })}
</p>
<figure className="my-6">
<Image
src="/monitor/terminal/search-commands.png"
alt={t("search.modalAlt")}
width={1600}
height={1200}
className="rounded-lg border border-gray-200 shadow-sm w-full h-auto"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t.rich("search.modalCaption", { code, em })}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">
<strong>{t("search.aboutLabel")}</strong>{" "}
<a
href="https://cheat.sh"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline inline-flex items-center gap-1"
>
cheat.sh
<ExternalLink className="w-3 h-3" />
</a>{" "}
{t.rich("search.aboutBody", { code })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("search.headerSource")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("search.headerWhen")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("search.headerWhat")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
<tr className="border-b border-gray-100">
<td className="px-3 py-2 align-top whitespace-nowrap">
<a
href="https://cheat.sh"
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline inline-flex items-center gap-1 font-semibold"
>
cheat.sh
<ExternalLink className="w-3 h-3" />
</a>{" "}
{t("search.onlineLabel")}
</td>
<td className="px-3 py-2 align-top">{t("search.onlineWhen")}</td>
<td className="px-3 py-2 align-top">{t.rich("search.onlineWhat", { green })}</td>
</tr>
<tr>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{t("search.fallbackLabel")}</strong></td>
<td className="px-3 py-2 align-top">{t("search.fallbackWhen")}</td>
<td className="px-3 py-2 align-top">{t.rich("search.fallbackWhat", { red })}</td>
</tr>
</tbody>
</table>
</div>
<p className="mb-6 text-gray-800 leading-relaxed text-sm">
{t.rich("search.sendingNote", { strong })}
</p>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("auth.heading")}</h2>
<ul className="list-disc pl-6 mb-6 text-gray-800 leading-relaxed space-y-1">
{authItems.map((_, idx) => (
<li key={idx}>{t.rich(`auth.items.${idx}`, { code, link: authLink })}</li>
))}
</ul>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("clipboard.heading")}</h2>
<ul className="list-disc pl-6 mb-6 text-gray-800 leading-relaxed space-y-1">
{clipboardItems.map((_, idx) => (
<li key={idx}>{t.rich(`clipboard.items.${idx}`, { strong, code })}</li>
))}
</ul>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("disconnect.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("disconnect.intro")}</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("disconnect.headerCause")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("disconnect.headerFix")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{disconnectRows.map((row, idx) => (
<tr key={row.cause} className={idx < disconnectRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top">{row.cause}</td>
<td className="px-3 py-2 align-top">{t.rich(`disconnect.rows.${idx}.fix`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<Callout variant="warning" title={t("warning.title")}>
{t.rich("warning.body", { code, authLink: authLinkWarn, gatewayLink })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("whereNext.heading")}</h2>
<ul className="list-disc pl-6 text-gray-800 leading-relaxed space-y-1">
{whereNextItems.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,437 @@
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.monitor.dashboard.vmsLxcs.meta" })
return { title: t("title"), description: t("description") }
}
type LifecycleRow = { button: string; color: string; enabled: string; action: string }
type DataRow = { section: string; endpoint: string; source: string }
type WhereNextItem = { label: string; href: string; tailRich: string }
export default async function VmsLxcsTabPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.monitor.dashboard.vmsLxcs" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { monitor: { dashboard: { vmsLxcs: {
topRow: { memoryItems: string[] }
inventory: { rows: string[] }
drillIn: {
liveItems: string[]
ioItems: string[]
resourcesItems: string[]
mountTypesItems: string[]
mountStateItems: string[]
backupsItems: string[]
updatesPanelItems: string[]
firewallItems: string[]
lifecycleRows: LifecycleRow[]
}
dataCollected: { rows: DataRow[] }
whereNext: { items: WhereNextItem[] }
} } } }
}
const v = messages.docs.monitor.dashboard.vmsLxcs
const memoryItems = v.topRow.memoryItems
const inventoryRows = v.inventory.rows
const liveItems = v.drillIn.liveItems
const ioItems = v.drillIn.ioItems
const resourcesItems = v.drillIn.resourcesItems
const mountTypesItems = v.drillIn.mountTypesItems
const mountStateItems = v.drillIn.mountStateItems
const backupsItems = v.drillIn.backupsItems
const updatesPanelItems = v.drillIn.updatesPanelItems
const firewallItems = v.drillIn.firewallItems
const lifecycleRows = v.drillIn.lifecycleRows
const dataRows = v.dataCollected.rows
const whereNextItems = v.whereNext.items
const code = (chunks: React.ReactNode) => <code>{chunks}</code>
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
const green = () => <span className="inline-block w-2 h-2 rounded-full bg-green-500 align-middle mr-1" />
const amber = () => <span className="inline-block w-2 h-2 rounded-full bg-amber-500 align-middle mr-1" />
const red = () => <span className="inline-block w-2 h-2 rounded-full bg-red-500 align-middle mr-1" />
const greenText = (chunks: React.ReactNode) => <span className="text-green-600 font-semibold">{chunks}</span>
const amberText = (chunks: React.ReactNode) => <span className="text-amber-600 font-semibold">{chunks}</span>
const redText = (chunks: React.ReactNode) => <span className="text-red-600 font-semibold">{chunks}</span>
const orangeText = (chunks: React.ReactNode) => <span className="text-orange-600 font-semibold">{chunks}</span>
const link = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/dashboard/terminal" className="text-blue-600 hover:underline">{chunks}</Link>
)
const buttonColorClass = (color: string) => {
switch (color) {
case "green":
return "text-green-600 font-semibold"
case "blue":
return "text-blue-600 font-semibold"
case "red":
return "text-red-600 font-semibold"
default:
return "font-semibold"
}
}
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={12}
/>
<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("topRow.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("topRow.intro")}</p>
<figure className="my-6">
<img
src="/monitor/vms-top-row.png"
alt={t("topRow.imageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("topRow.imageCaption")}
</figcaption>
</figure>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("topRow.headerCard")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("topRow.headerWhat")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
<tr className="border-b border-gray-100">
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{t("topRow.totalLabel")}</strong></td>
<td className="px-3 py-2 align-top">{t.rich("topRow.totalWhat", { em })}</td>
</tr>
<tr className="border-b border-gray-100">
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{t("topRow.cpuLabel")}</strong></td>
<td className="px-3 py-2 align-top">{t.rich("topRow.cpuWhat", { em })}</td>
</tr>
<tr className="border-b border-gray-100">
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{t("topRow.memoryLabel")}</strong></td>
<td className="px-3 py-2 align-top">
{t("topRow.memoryIntro")}
<ul className="list-disc pl-5 mt-2 space-y-0.5">
{memoryItems.map((_, idx) => (
<li key={idx}>{t.rich(`topRow.memoryItems.${idx}`, { strong, em, code })}</li>
))}
</ul>
</td>
</tr>
<tr>
<td className="px-3 py-2 align-top whitespace-nowrap"><strong>{t("topRow.diskLabel")}</strong></td>
<td className="px-3 py-2 align-top">{t.rich("topRow.diskWhat", { em })}</td>
</tr>
</tbody>
</table>
</div>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("inventory.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("inventory.intro", { code })}
</p>
<figure className="my-6">
<img
src="/monitor/vms-inventory-mobile.png"
alt={t("inventory.imageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full max-w-md mx-auto"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("inventory.imageCaption")}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">{t("inventory.rowsIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{inventoryRows.map((_, idx) => (
<li key={idx}>{t.rich(`inventory.rows.${idx}`, { strong, em })}</li>
))}
</ul>
<p className="mb-6 text-gray-800 leading-relaxed">{t("inventory.clickHint")}</p>
<Callout variant="tip" title={t("inventory.mobileTitle")}>
{t("inventory.mobileBody")}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("drillIn.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("drillIn.intro", { strong, em })}
</p>
<h3 className="text-lg font-semibold mt-8 mb-2 text-gray-900">{t("drillIn.statusTitle")}</h3>
<figure className="my-4">
<img
src="/monitor/vms-modal-status.png"
alt={t("drillIn.statusImageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("drillIn.statusImageCaption")}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">{t("drillIn.statusIntro")}</p>
<h4 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("drillIn.liveTitle")}</h4>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{liveItems.map((_, idx) => (
<li key={idx}>{t.rich(`drillIn.liveItems.${idx}`, { strong, em })}</li>
))}
</ul>
<h4 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("drillIn.ioTitle")}</h4>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{ioItems.map((_, idx) => (
<li key={idx}>{t.rich(`drillIn.ioItems.${idx}`, { strong })}</li>
))}
</ul>
<h4 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("drillIn.resourcesTitle")}</h4>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("drillIn.resourcesIntro", { code })}
</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{resourcesItems.map((_, idx) => (
<li key={idx}>{t.rich(`drillIn.resourcesItems.${idx}`, { strong, code })}</li>
))}
</ul>
<h4 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("drillIn.ipsTitle")}</h4>
<p className="mb-6 text-gray-800 leading-relaxed">{t("drillIn.ipsBody")}</p>
<h3 className="text-lg font-semibold mt-8 mb-2 text-gray-900">{t("drillIn.mountsTitle")}</h3>
<figure className="my-4">
<img
src="/monitor/vms-modal-mounts.png"
alt={t("drillIn.mountsImageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("drillIn.mountsImageCaption")}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("drillIn.mountsIntro", { strong, code })}
</p>
<h4 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("drillIn.mountTypesTitle")}</h4>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{mountTypesItems.map((_, idx) => (
<li key={idx}>{t.rich(`drillIn.mountTypesItems.${idx}`, { strong, em, code })}</li>
))}
</ul>
<h4 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("drillIn.mountStateTitle")}</h4>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{mountStateItems.map((_, idx) => (
<li key={idx}>
{t.rich(`drillIn.mountStateItems.${idx}`, { strong, em, code, green, amber, red })}
</li>
))}
</ul>
<Callout variant="info" title={t("drillIn.mountsCalloutTitle")}>
{t("drillIn.mountsCalloutBody")}
</Callout>
<h3 className="text-lg font-semibold mt-8 mb-2 text-gray-900">{t("drillIn.backupsTitle")}</h3>
<figure className="my-4">
<img
src="/monitor/vms-modal-backups.png"
alt={t("drillIn.backupsImageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("drillIn.backupsImageCaption")}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">{t("drillIn.backupsIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{backupsItems.map((_, idx) => (
<li key={idx}>{t.rich(`drillIn.backupsItems.${idx}`, { strong })}</li>
))}
</ul>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("drillIn.backupsOutro", { strong })}
</p>
<h3 className="text-lg font-semibold mt-8 mb-2 text-gray-900">{t("drillIn.updatesTitle")}</h3>
<figure className="my-4">
<img
src="/monitor/vms-modal-lxc-updates.png"
alt={t("drillIn.updatesImageAlt")}
className="rounded-lg border border-gray-200 shadow-sm w-full"
/>
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">
{t("drillIn.updatesImageCaption")}
</figcaption>
</figure>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("drillIn.updatesIntro", { strong, code })}
</p>
<h4 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("drillIn.updatesPanelTitle")}</h4>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{updatesPanelItems.map((_, idx) => (
<li key={idx}>{t.rich(`drillIn.updatesPanelItems.${idx}`, { strong })}</li>
))}
</ul>
<h4 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("drillIn.updatesScopeTitle")}</h4>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("drillIn.updatesScopeBody", { strong, em, code })}
</p>
<h4 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("drillIn.updatesToggleTitle")}</h4>
<Callout variant="info" title={t("drillIn.updatesToggleCalloutTitle")}>
{t.rich("drillIn.updatesToggleCalloutBody", { strong, code })}
</Callout>
<h4 className="text-base font-semibold mt-6 mb-2 text-gray-900">{t("drillIn.updatesApplyTitle")}</h4>
<p className="mb-6 text-gray-800 leading-relaxed">
{t.rich("drillIn.updatesApplyBody", { code })}
</p>
<h3 className="text-lg font-semibold mt-8 mb-2 text-gray-900">{t("drillIn.firewallTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">{t("drillIn.firewallIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{firewallItems.map((_, idx) => (
<li key={idx}>
{t.rich(`drillIn.firewallItems.${idx}`, {
strong,
em,
code,
green: greenText,
orange: orangeText,
red: redText,
})}
</li>
))}
</ul>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("drillIn.firewallRefresh", { em, code })}
</p>
<Callout variant="info" title={t("drillIn.firewallCalloutTitle")}>
{t("drillIn.firewallCalloutBody")}
</Callout>
<h3 className="text-lg font-semibold mt-8 mb-2 text-gray-900">{t("drillIn.actionBarTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">{t("drillIn.actionBarIntro")}</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
<li>{t.rich("drillIn.consoleItem", { strong, code, link })}</li>
</ul>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("drillIn.lifecycleIntro", { code })}
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("drillIn.headerButton")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("drillIn.headerEnabled")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("drillIn.headerAction")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{lifecycleRows.map((row, idx) => (
<tr key={row.button} className={idx < lifecycleRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top whitespace-nowrap">
<strong className={buttonColorClass(row.color)}>{row.button}</strong>
</td>
<td className="px-3 py-2 align-top">{row.enabled}</td>
<td className="px-3 py-2 align-top font-mono text-xs">{row.action}</td>
</tr>
))}
</tbody>
</table>
</div>
<Callout variant="warning" title={t("drillIn.forceStopTitle")}>
{t.rich("drillIn.forceStopBody", { strong })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("dataCollected.heading")}</h2>
<div className="overflow-x-auto mb-6">
<table className="w-full text-sm border border-gray-200 rounded-md">
<thead className="bg-gray-50 text-gray-900">
<tr>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerSection")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerEndpoint")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("dataCollected.headerSource")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{dataRows.map((row, idx) => (
<tr key={row.section} className={idx < dataRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top">{row.section}</td>
<td className="px-3 py-2 align-top font-mono text-xs">{row.endpoint}</td>
<td className="px-3 py-2 align-top">{t.rich(`dataCollected.rows.${idx}.source`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<CopyableCode
code={`${t("dataCollected.codeComment1")}
pvesh get /cluster/resources --type vm --output-format=json | jq
${t("dataCollected.codeComment2")}
qm config 100 ${t("dataCollected.codeComment3")}
pct config 100 ${t("dataCollected.codeComment4")}`}
className="my-4"
/>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("whereNext.heading")}</h2>
<ul className="list-disc pl-6 text-gray-800 leading-relaxed space-y-1">
{whereNextItems.map((item, idx) => (
<li key={item.href}>
<Link href={item.href} className="text-blue-600 hover:underline">
{item.label}
</Link>
{t.rich(`whereNext.items.${idx}.tailRich`, { code })}
</li>
))}
</ul>
</div>
)
}