mirror of
https://github.com/MacRimi/ProxMenux.git
synced 2026-06-14 20:36:59 +00:00
complete i18n migration to /[locale]/ with EN+ES content
Full rewrite of the docs site under app/[locale]/ with next-intl in localePrefix:"always" mode. Every page now exists at both /en/<path> and /es/<path>; the root / shows a meta-refresh + JS redirect to /<defaultLocale>/ so GitHub Pages serves something on the apex URL. Highlights: - 107 doc pages migrated to file-per-page JSON namespaces under messages/en/ and messages/es/. Spanish content is fully translated (no copy-of-English placeholders). - New documentation for the Active Suppressions section in the Settings tab and the per-event Dismiss dropdown in the Health Monitor modal. - New screenshots: dismiss-duration-dropdown.png and an updated health-suppression-settings.png. - Pagefind integrated for client-side search; index is built on every CI deploy (not committed). - RSS feeds: per-locale at /<locale>/rss.xml plus root /rss.xml for backward compat. - Removed the dead app/[locale]/guides/[slug]/ route — every guide now has its own static page and no markdown source remains. - Fixed orphan link /guides/nvidia -> /guides/nvidia-manual in docs/hardware/nvidia-host. - Removed obsolete components (footer2, calendar, drawer). Verified locally with `npm ci && npm run build`: 2804 files in out/, 231 pages indexed by pagefind, root redirect intact, both locale roots and the new Active Suppressions docs render OK.
This commit is contained in:
518
web/app/[locale]/docs/monitor/dashboard/hardware/page.tsx
Normal file
518
web/app/[locale]/docs/monitor/dashboard/hardware/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
325
web/app/[locale]/docs/monitor/dashboard/network/page.tsx
Normal file
325
web/app/[locale]/docs/monitor/dashboard/network/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
120
web/app/[locale]/docs/monitor/dashboard/page.tsx
Normal file
120
web/app/[locale]/docs/monitor/dashboard/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
489
web/app/[locale]/docs/monitor/dashboard/security/page.tsx
Normal file
489
web/app/[locale]/docs/monitor/dashboard/security/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
486
web/app/[locale]/docs/monitor/dashboard/settings/page.tsx
Normal file
486
web/app/[locale]/docs/monitor/dashboard/settings/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
474
web/app/[locale]/docs/monitor/dashboard/storage/page.tsx
Normal file
474
web/app/[locale]/docs/monitor/dashboard/storage/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
178
web/app/[locale]/docs/monitor/dashboard/system-logs/page.tsx
Normal file
178
web/app/[locale]/docs/monitor/dashboard/system-logs/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
225
web/app/[locale]/docs/monitor/dashboard/system-overview/page.tsx
Normal file
225
web/app/[locale]/docs/monitor/dashboard/system-overview/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
319
web/app/[locale]/docs/monitor/dashboard/terminal/page.tsx
Normal file
319
web/app/[locale]/docs/monitor/dashboard/terminal/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
437
web/app/[locale]/docs/monitor/dashboard/vms-lxcs/page.tsx
Normal file
437
web/app/[locale]/docs/monitor/dashboard/vms-lxcs/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user