Files
ProxMenux/web/app/[locale]/docs/monitor/integrations/page.tsx
T
MacRimi 5ca3463bf6 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.
2026-05-31 12:41:10 +02:00

1165 lines
40 KiB
TypeScript

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.integrations.meta" })
return {
title: t("title"),
description: t("description"),
keywords: [
"proxmox homepage integration",
"proxmox home assistant",
"proxmox grafana",
"proxmox prometheus",
"proxmox uptime kuma",
"proxmox dashboard",
"proxmenux integrations",
"proxmox custom api widget",
"proxmox rest sensor",
],
alternates: { canonical: "https://proxmenux.com/docs/monitor/integrations" },
openGraph: {
title: t("ogTitle"),
description: t("ogDescription"),
type: "article",
url: "https://proxmenux.com/docs/monitor/integrations",
},
twitter: {
card: "summary",
title: t("twitterTitle"),
description: t("twitterDescription"),
},
}
}
type Row2 = { query: string; confirms: string }
type Row3 = { panel: string; promql: string }
type WhereNextItem = { label: string; href: string; tail: string }
export default async function MonitorIntegrationsPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.monitor.integrations" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { monitor: { integrations: {
auth: { httpsItems: string[] }
homeAssistant: {
altViewSteps: string[]
twoEditorsItems: string[]
logoBrokenSteps: string[]
}
grafana: { verifyRows: Row2[]; panelRows: Row3[] }
uptimeKuma: { kumaSteps: string[] }
whereNext: { items: WhereNextItem[] }
} } }
}
const i = messages.docs.monitor.integrations
const httpsItems = i.auth.httpsItems
const altViewSteps = i.homeAssistant.altViewSteps
const twoEditorsItems = i.homeAssistant.twoEditorsItems
const logoBrokenSteps = i.homeAssistant.logoBrokenSteps
const verifyRows = i.grafana.verifyRows
const panelRows = i.grafana.panelRows
const kumaSteps = i.uptimeKuma.kumaSteps
const whereNextItems = i.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 apiLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/api" className="text-blue-600 hover:underline">{chunks}</Link>
)
const accessLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/access-auth" className="text-blue-600 hover:underline">{chunks}</Link>
)
const promAnchor = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/api#prometheus" className="text-blue-600 hover:underline">{chunks}</Link>
)
const notifEventsLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/notifications" className="text-blue-600 hover:underline">{chunks}</Link>
)
const pveLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/notifications#pve-webhook-integration" className="text-blue-600 hover:underline">{chunks}</Link>
)
const ext = (href: string) => (chunks: React.ReactNode) =>
(
<a href={href} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline inline-flex items-center gap-1">
{chunks}
<ExternalLink className="w-3 h-3" />
</a>
)
return (
<div>
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={20}
/>
<Callout variant="info" title={t("intro.title")}>
{t.rich("intro.body", { link: apiLink })}
</Callout>
<h2 id="authentication" className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("auth.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("auth.intro")}</p>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("auth.optAtitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("auth.optAbody1", { strong, em })}
</p>
<p className="mb-4 text-gray-800 leading-relaxed">{t("auth.optAbody2")}</p>
<CopyableCode
code={`curl -H "Authorization: Bearer <api-token>" http://<host>:8008/api/system | jq`}
className="my-4"
/>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("auth.optBtitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">{t("auth.optBbody")}</p>
<CopyableCode
code={`# 1. Exchange credentials for a JWT
curl -X POST http://<host>:8008/api/auth/login \\
-H "Content-Type: application/json" \\
-d '{"username":"admin","password":"<your-password>","totp_token":"123456"}' | jq -r '.token'
# 2. Use the returned token exactly like an API token
curl -H "Authorization: Bearer <returned-token>" http://<host>:8008/api/system | jq`}
className="my-4"
/>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("auth.outro", { link: accessLink })}
</p>
<Callout variant="info" title={t("auth.httpsTitle")}>
{t.rich("auth.httpsIntro", { code, strong })}
<ul className="list-disc pl-6 space-y-1 mb-0 mt-2">
{httpsItems.map((_, idx) => (
<li key={idx}>{t.rich(`auth.httpsItems.${idx}`, { strong, code })}</li>
))}
</ul>
</Callout>
<h2 id="homepage" className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
<a href={t("homepage.headingHref")} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline inline-flex items-center gap-1">
{t("homepage.heading")}<ExternalLink className="w-5 h-5" />
</a>
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("homepage.intro", { code })}
</p>
<Callout variant="tip" title={t("homepage.iconCalloutTitle")}>
{t.rich("homepage.iconCalloutBody", {
code,
a1: ext("https://dashboardicons.com"),
a2: ext("https://dashboardicons.com/icons/external/proxmenux"),
})}
</Callout>
<figure className="my-6">
<Image src="/monitor/integrations/homepage.png" alt={t("homepage.imageAlt")} width={2000} height={1062} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto mx-auto max-w-2xl" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t.rich("homepage.imageCaption", { code })}</figcaption>
</figure>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("homepage.basicTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("homepage.basicIntro", { code })}
</p>
<CopyableCode
code={`- ProxMenux Monitor:
href: http://proxmox.example.tld:8008/
icon: proxmenux.png
widget:
type: customapi
url: http://proxmox.example.tld:8008/api/system
refreshInterval: 10000
mappings:
- field: uptime
label: Uptime
icon: lucide:clock-4
format: text
- field: cpu_usage
label: CPU
icon: lucide:cpu
format: percent
- field: memory_usage
label: RAM
icon: lucide:memory-stick
format: percent
- field: temperature
label: Temp
icon: lucide:thermometer-sun
format: number
suffix: °C`}
className="my-4"
/>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("homepage.authedTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("homepage.authedIntro", { strong, code })}
</p>
<CopyableCode
code={`- ProxMenux Monitor:
href: http://proxmox.example.tld:8008/
icon: proxmenux.png
widget:
type: customapi
url: http://proxmox.example.tld:8008/api/system
headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.abc123def456ghi789
refreshInterval: 10000
mappings:
- field: uptime
label: Uptime
icon: lucide:clock-4
format: text
- field: cpu_usage
label: CPU
icon: lucide:cpu
format: percent
- field: memory_usage
label: RAM
icon: lucide:memory-stick
format: percent
- field: temperature
label: Temp
icon: lucide:thermometer-sun
format: number
suffix: °C`}
className="my-4"
/>
<p className="mb-4 text-gray-800 leading-relaxed">{t("homepage.authedOutro")}</p>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("homepage.multiTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">{t("homepage.multiIntro")}</p>
<CopyableCode
code={`- ProxMenux System:
href: http://proxmox.example.tld:8008/
icon: lucide:server
description: Proxmox VE Host
widget:
type: customapi
url: http://proxmox.example.tld:8008/api/system
headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.abc123def456ghi789
refreshInterval: 5000
mappings:
- field: cpu_usage
label: CPU
icon: lucide:cpu
format: percent
- field: memory_usage
label: RAM
icon: lucide:memory-stick
format: percent
- field: temperature
label: Temp
icon: lucide:thermometer-sun
format: number
suffix: °C
- ProxMenux Storage:
href: http://proxmox.example.tld:8008/#/storage
icon: lucide:hard-drive
description: Storage Overview
widget:
type: customapi
url: http://proxmox.example.tld:8008/api/storage/summary
headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.abc123def456ghi789
refreshInterval: 30000
mappings:
- field: total
label: Total
icon: lucide:database
format: number
suffix: " TB"
- field: used
label: Used
icon: lucide:folder
format: number
suffix: " GB"
- field: disk_count
label: Disks
icon: lucide:hard-drive
format: number
- ProxMenux Network:
href: http://proxmox.example.tld:8008/#/network
icon: lucide:network
description: Network Stats
widget:
type: customapi
url: http://proxmox.example.tld:8008/api/network/summary
headers:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.abc123def456ghi789
refreshInterval: 5000
mappings:
- field: traffic.bytes_recv
label: Received
icon: lucide:download
format: bytes
- field: traffic.bytes_sent
label: Sent
icon: lucide:upload
format: bytes
- field: physical_active_count
label: NICs up
icon: lucide:network
format: number`}
className="my-4"
/>
<Callout variant="tip" title={t("homepage.multiCalloutTitle")}>
{t.rich("homepage.multiCalloutBody", { code })}
</Callout>
<h2 id="home-assistant" className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
<a href={t("homeAssistant.headingHref")} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline inline-flex items-center gap-1">
{t("homeAssistant.heading")}<ExternalLink className="w-5 h-5" />
</a>
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("homeAssistant.intro", { code, link: apiLink })}
</p>
<figure className="my-6">
<Image src="/monitor/integrations/home-assistant.png" alt={t("homeAssistant.imageAlt")} width={2000} height={1200} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto mx-auto max-w-2xl" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t("homeAssistant.imageCaption")}</figcaption>
</figure>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("homeAssistant.step1Title")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("homeAssistant.step1Body", { code })}
</p>
<CopyableCode
code={`# secrets.yaml — add this file to .gitignore
proxmenux_token_header: "Bearer <your_actual_api_token_here>"`}
className="my-4"
/>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("homeAssistant.step2Title")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("homeAssistant.step2Body", { code })}
</p>
<CopyableCode
code={`rest:
# ─── Block 1: system resources (CPU, memory, temperature, uptime, host info) ───
- resource: http://<proxmenux-host>:8008/api/system
headers:
Authorization: !secret proxmenux_token_header
scan_interval: 30
sensor:
- name: "ProxMenux CPU"
unique_id: proxmenux_cpu
value_template: "{{ value_json.cpu_usage }}"
unit_of_measurement: "%"
state_class: measurement
icon: mdi:cpu-64-bit
- name: "ProxMenux RAM"
unique_id: proxmenux_ram
value_template: "{{ value_json.memory_usage }}"
unit_of_measurement: "%"
state_class: measurement
icon: mdi:memory
- name: "ProxMenux Memory Used"
unique_id: proxmenux_memory_used
value_template: "{{ value_json.memory_used }}"
unit_of_measurement: "GB"
state_class: measurement
icon: mdi:memory
- name: "ProxMenux Memory Total"
unique_id: proxmenux_memory_total
value_template: "{{ value_json.memory_total }}"
unit_of_measurement: "GB"
icon: mdi:memory
- name: "ProxMenux CPU Temperature"
unique_id: proxmenux_cpu_temperature
value_template: "{{ value_json.temperature }}"
unit_of_measurement: "°C"
device_class: temperature
state_class: measurement
- name: "ProxMenux Uptime"
unique_id: proxmenux_uptime
value_template: "{{ value_json.uptime }}"
icon: mdi:clock-outline
- name: "ProxMenux Load 1m"
unique_id: proxmenux_load_1m
value_template: "{{ value_json.load_average[0] | round(2) }}"
state_class: measurement
icon: mdi:gauge
- name: "ProxMenux Available Updates"
unique_id: proxmenux_available_updates
value_template: "{{ value_json.available_updates }}"
state_class: measurement
icon: mdi:package-up
- name: "ProxMenux Host"
unique_id: proxmenux_host
value_template: "{{ value_json.hostname }}"
json_attributes:
- kernel_version
- proxmox_version
- cpu_cores
- cpu_threads
# ─── Block 2: Health Monitor (overall + per-category + active errors) ───
- resource: http://<proxmenux-host>:8008/api/health/full
headers:
Authorization: !secret proxmenux_token_header
scan_interval: 60
sensor:
- name: "ProxMenux Health"
unique_id: proxmenux_health_overall
value_template: "{{ value_json.health.overall }}"
json_attributes_path: "$.health"
json_attributes:
- summary
- details
icon: mdi:heart-pulse
- name: "ProxMenux Active Errors"
unique_id: proxmenux_active_errors
value_template: "{{ value_json.active_errors | length }}"
state_class: measurement
icon: mdi:alert-circle
json_attributes:
- active_errors
- name: "ProxMenux Dismissed Errors"
unique_id: proxmenux_dismissed_errors
value_template: "{{ value_json.dismissed | length }}"
state_class: measurement
icon: mdi:alert-circle-outline
# ─── Block 3: VMs and containers ───
- resource: http://<proxmenux-host>:8008/api/vms
headers:
Authorization: !secret proxmenux_token_header
scan_interval: 60
sensor:
- name: "ProxMenux VMs Total"
unique_id: proxmenux_vms_total
value_template: "{{ value_json | length }}"
state_class: measurement
icon: mdi:server
json_attributes:
- vms
- name: "ProxMenux VMs Running"
unique_id: proxmenux_vms_running
value_template: >
{{ value_json | selectattr('status', 'eq', 'running') | list | length }}
state_class: measurement
icon: mdi:play-circle
- name: "ProxMenux VMs Stopped"
unique_id: proxmenux_vms_stopped
value_template: >
{{ value_json | selectattr('status', 'eq', 'stopped') | list | length }}
state_class: measurement
icon: mdi:stop-circle
# ─── Block 4: Storage summary ───
- resource: http://<proxmenux-host>:8008/api/storage/summary
headers:
Authorization: !secret proxmenux_token_header
scan_interval: 300
sensor:
- name: "ProxMenux Storage Total"
unique_id: proxmenux_storage_total
value_template: "{{ value_json.total }}"
unit_of_measurement: "TB"
icon: mdi:harddisk
- name: "ProxMenux Storage Used"
unique_id: proxmenux_storage_used
value_template: "{{ value_json.used }}"
unit_of_measurement: "GB"
state_class: measurement
icon: mdi:harddisk
- name: "ProxMenux Storage Available"
unique_id: proxmenux_storage_available
value_template: "{{ value_json.available }}"
unit_of_measurement: "GB"
state_class: measurement
- name: "ProxMenux Disk Count"
unique_id: proxmenux_disk_count
value_template: "{{ value_json.disk_count }}"
# ─── Block 5: Network summary ───
- resource: http://<proxmenux-host>:8008/api/network/summary
headers:
Authorization: !secret proxmenux_token_header
scan_interval: 30
sensor:
- name: "ProxMenux Net Rx Bytes"
unique_id: proxmenux_net_rx_bytes
value_template: "{{ value_json.traffic.bytes_recv }}"
unit_of_measurement: "B"
device_class: data_size
state_class: total_increasing
- name: "ProxMenux Net Tx Bytes"
unique_id: proxmenux_net_tx_bytes
value_template: "{{ value_json.traffic.bytes_sent }}"
unit_of_measurement: "B"
device_class: data_size
state_class: total_increasing
- name: "ProxMenux Physical NICs Up"
unique_id: proxmenux_physical_nics_up
value_template: >
{{ value_json.physical_active_count }} / {{ value_json.physical_total_count }}
icon: mdi:ethernet
- name: "ProxMenux Bridges Up"
unique_id: proxmenux_bridges_up
value_template: >
{{ value_json.bridge_active_count }} / {{ value_json.bridge_total_count }}
icon: mdi:bridge
# ─── Block 6: ProxMenux update availability ───
- resource: http://<proxmenux-host>:8008/api/proxmenux/update-status
headers:
Authorization: !secret proxmenux_token_header
scan_interval: 3600
sensor:
- name: "ProxMenux Monitor Update"
unique_id: proxmenux_monitor_update
value_template: >
{{ 'update available' if (value_json.stable or value_json.beta) else 'up to date' }}
json_attributes:
- stable
- stable_version
- beta
- beta_version
icon: mdi:update`}
className="my-4"
/>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("homeAssistant.step3Title")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">{t("homeAssistant.step3Body")}</p>
<CopyableCode
code={`# configuration.yaml — add alongside the rest: block above
# All four template entities live under one template: section (HA modern syntax).
# If you already have a template: section elsewhere, merge the lists into it
# rather than declaring template: a second time.
template:
- binary_sensor:
- name: "ProxMenux Healthy"
default_entity_id: binary_sensor.proxmenux_is_healthy
device_class: problem
delay_off: 30
state: "{{ states('sensor.proxmenux_health') == 'OK' }}"
- name: "ProxMenux Critical"
default_entity_id: binary_sensor.proxmenux_has_critical
device_class: safety
state: "{{ states('sensor.proxmenux_health') == 'CRITICAL' }}"
- sensor:
- name: "ProxMenux Memory Free"
unique_id: proxmenux_memory_free
unit_of_measurement: "GB"
state_class: measurement
state: >
{{ (states('sensor.proxmenux_memory_total') | float
- states('sensor.proxmenux_memory_used') | float) | round(1) }}
- name: "ProxMenux Storage Usage Percent"
unique_id: proxmenux_storage_usage_percent
unit_of_measurement: "%"
state_class: measurement
state: >
{% set used = states('sensor.proxmenux_storage_used') | float %}
{% set free = states('sensor.proxmenux_storage_available') | float %}
{% set total = used + free %}
{{ (used / total * 100) | round(1) if total > 0 else 0 }}`}
className="my-4"
/>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("homeAssistant.step4Title")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("homeAssistant.step4Body", { em })}
</p>
<Callout variant="warning" title={t("homeAssistant.replaceTitle")}>
{t.rich("homeAssistant.replaceBody", { em, code })}
</Callout>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("homeAssistant.step5Title")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("homeAssistant.step5Body", { strong, em })}
</p>
<CopyableCode
code={`type: vertical-stack
cards:
# Header — ProxMenux logo + live health status overlay
- type: picture-entity
entity: sensor.proxmenux_health
name: ProxMenux Monitor
image: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/proxmenux.svg
show_state: true
# Quick KPIs
- type: glance
title: ProxMenux — at a glance
columns: 4
entities:
- entity: sensor.proxmenux_health
name: Health
- entity: sensor.proxmenux_cpu
name: CPU
- entity: sensor.proxmenux_ram
name: RAM
- entity: sensor.proxmenux_cpu_temperature
name: Temp
# System detail
- type: entities
title: System
entities:
- entity: sensor.proxmenux_host
name: Hostname
- entity: sensor.proxmenux_uptime
name: Uptime
- entity: sensor.proxmenux_load_1m
name: Load avg (1m)
- entity: sensor.proxmenux_memory_used
name: RAM used
- entity: sensor.proxmenux_memory_total
name: RAM total
- entity: sensor.proxmenux_memory_free
name: RAM free
- entity: sensor.proxmenux_available_updates
name: APT updates pending
# VMs / CTs
- type: glance
title: "VMs & Containers"
columns: 3
entities:
- entity: sensor.proxmenux_vms_total
name: Total
- entity: sensor.proxmenux_vms_running
name: Running
- entity: sensor.proxmenux_vms_stopped
name: Stopped
# Storage
- type: entities
title: Storage
entities:
- entity: sensor.proxmenux_storage_total
name: Total disks (TB)
- entity: sensor.proxmenux_storage_used
name: Used (GB)
- entity: sensor.proxmenux_storage_available
name: Available (GB)
- entity: sensor.proxmenux_storage_usage_percent
name: Usage
- entity: sensor.proxmenux_disk_count
name: Disk count
# Network
- type: entities
title: Network
entities:
- entity: sensor.proxmenux_physical_nics_up
name: Physical interfaces
- entity: sensor.proxmenux_bridges_up
name: Bridges
- entity: sensor.proxmenux_net_rx_bytes
name: Received
- entity: sensor.proxmenux_net_tx_bytes
name: Sent
# Health detail — only render when there are active errors
- type: conditional
conditions:
- entity: sensor.proxmenux_active_errors
state_not: "0"
card:
type: entities
title: Active health issues
entities:
- entity: sensor.proxmenux_active_errors
name: Active error count
- entity: sensor.proxmenux_dismissed_errors
name: Dismissed (in suppression window)
- entity: binary_sensor.proxmenux_has_critical
name: Critical present?`}
className="my-4"
/>
<Callout variant="tip" title={t("homeAssistant.viewTipTitle")}>
{t.rich("homeAssistant.viewTipBody", { em, code })}
<pre className="mt-2 rounded-md bg-white border border-slate-200 p-3 overflow-x-auto text-xs font-mono text-gray-800">{`- title: ProxMenux Monitor
icon: mdi:server
cards:
# ... paste the cards from the vertical-stack above, without the
# outer "type: vertical-stack" wrapper, indented one extra level
`}</pre>
{t("homeAssistant.viewTipOutro")}
</Callout>
<h4 className="text-base font-semibold mt-8 mb-2 text-gray-900">{t("homeAssistant.altViewTitle")}</h4>
<p className="mb-4 text-gray-800 leading-relaxed">{t("homeAssistant.altViewIntro")}</p>
<ol className="list-decimal pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{altViewSteps.map((_, idx) => (
<li key={idx}>{t.rich(`homeAssistant.altViewSteps.${idx}`, { em })}</li>
))}
</ol>
<Callout variant="warning" title={t("homeAssistant.twoEditorsTitle")}>
{t("homeAssistant.twoEditorsIntro")}
<ul className="list-disc pl-6 mb-0 mt-2 space-y-1">
{twoEditorsItems.map((_, idx) => (
<li key={idx}>{t.rich(`homeAssistant.twoEditorsItems.${idx}`, { strong, em, code })}</li>
))}
</ul>
{t.rich("homeAssistant.twoEditorsOutro", { em, code })}
</Callout>
<CopyableCode
code={`title: ProxMenux Monitor
path: proxmenux
icon: mdi:server
cards:
# Header — full width
- type: picture-entity
entity: sensor.proxmenux_health
name: ProxMenux Monitor
image: https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/proxmenux.svg
show_state: true
# Quick KPIs — full width
- type: glance
title: ProxMenux — at a glance
columns: 4
entities:
- entity: sensor.proxmenux_health
name: Health
- entity: sensor.proxmenux_cpu
name: CPU
- entity: sensor.proxmenux_ram
name: RAM
- entity: sensor.proxmenux_cpu_temperature
name: Temp
# System detail
- type: entities
title: System
entities:
- entity: sensor.proxmenux_host
name: Hostname
- entity: sensor.proxmenux_uptime
name: Uptime
- entity: sensor.proxmenux_load_1m
name: Load avg (1m)
- entity: sensor.proxmenux_memory_used
name: RAM used
- entity: sensor.proxmenux_memory_total
name: RAM total
- entity: sensor.proxmenux_memory_free
name: RAM free
- entity: sensor.proxmenux_available_updates
name: APT updates pending
# VMs / CTs
- type: glance
title: "VMs & Containers"
columns: 3
entities:
- entity: sensor.proxmenux_vms_total
name: Total
- entity: sensor.proxmenux_vms_running
name: Running
- entity: sensor.proxmenux_vms_stopped
name: Stopped
# Storage
- type: entities
title: Storage
entities:
- entity: sensor.proxmenux_storage_total
name: Total disks (TB)
- entity: sensor.proxmenux_storage_used
name: Used (GB)
- entity: sensor.proxmenux_storage_available
name: Available (GB)
- entity: sensor.proxmenux_storage_usage_percent
name: Usage
- entity: sensor.proxmenux_disk_count
name: Disk count
# Network
- type: entities
title: Network
entities:
- entity: sensor.proxmenux_physical_nics_up
name: Physical interfaces
- entity: sensor.proxmenux_bridges_up
name: Bridges
- entity: sensor.proxmenux_net_rx_bytes
name: Received
- entity: sensor.proxmenux_net_tx_bytes
name: Sent
# Health detail — only render when there are active errors
- type: conditional
conditions:
- entity: sensor.proxmenux_active_errors
state_not: "0"
card:
type: entities
title: Active health issues
entities:
- entity: sensor.proxmenux_active_errors
name: Active error count
- entity: sensor.proxmenux_dismissed_errors
name: Dismissed (in suppression window)
- entity: binary_sensor.proxmenux_has_critical
name: Critical present?`}
className="my-4"
/>
<figure className="my-6">
<Image src="/monitor/integrations/home-assistant-view.png" alt={t("homeAssistant.viewImageAlt")} width={2210} height={1606} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto mx-auto max-w-3xl" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t.rich("homeAssistant.viewImageCaption", { em })}</figcaption>
</figure>
<Callout variant="tip" title={t("homeAssistant.twoColTipTitle")}>
{t.rich("homeAssistant.twoColTipBody", { em, code })}
<pre className="mt-2 rounded-md bg-white border border-slate-200 p-3 overflow-x-auto text-xs font-mono text-gray-800">{` - type: horizontal-stack
cards:
- type: entities
title: System
entities: [...]
- type: glance
title: "VMs & Containers"
entities: [...]`}</pre>
{t("homeAssistant.twoColTipOutro")}
</Callout>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("homeAssistant.step6Title")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("homeAssistant.step6Body", { code })}
</p>
<CopyableCode
code={`# automations.yaml
- alias: "ProxMenux — health degraded to WARNING"
trigger:
- platform: state
entity_id: sensor.proxmenux_health
to: "WARNING"
action:
- service: notify.mobile_app_<your_phone>
data:
title: "Proxmox: warning"
message: >
{{ state_attr('sensor.proxmenux_health', 'summary') }}
- alias: "ProxMenux — health CRITICAL"
trigger:
- platform: state
entity_id: sensor.proxmenux_health
to: "CRITICAL"
action:
- service: notify.mobile_app_<your_phone>
data:
title: "🚨 Proxmox CRITICAL"
message: >
{{ state_attr('sensor.proxmenux_health', 'summary') }}
- service: persistent_notification.create
data:
title: "Proxmox CRITICAL"
message: >
{{ state_attr('sensor.proxmenux_health', 'summary') }}
notification_id: proxmenux_critical
- alias: "ProxMenux — VM unexpectedly stopped"
trigger:
- platform: numeric_state
entity_id: sensor.proxmenux_vms_stopped
above: 0
for: "00:02:00"
action:
- service: notify.mobile_app_<your_phone>
data:
title: "Proxmox: VM stopped"
message: >
{{ states('sensor.proxmenux_vms_stopped') }} VM(s) currently stopped`}
className="my-4"
/>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("homeAssistant.logoTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("homeAssistant.logoBody", {
a1: ext("https://dashboardicons.com"),
a2: ext("https://dashboardicons.com/icons/external/proxmenux"),
})}
</p>
<Callout variant="tip" title={t("homeAssistant.logoBrokenTitle")}>
{t("homeAssistant.logoBrokenIntro")}
<ol className="list-decimal pl-6 mb-0 mt-2 space-y-1">
{logoBrokenSteps.map((_, idx) => (
<li key={idx}>
{t.rich(`homeAssistant.logoBrokenSteps.${idx}`, {
code,
a: ext("https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/proxmenux.svg"),
})}
</li>
))}
</ol>
</Callout>
<Callout variant="tip" title={t("homeAssistant.scanTipTitle")}>
{t.rich("homeAssistant.scanTipBody", { code })}
</Callout>
<h2 id="prometheus-grafana" className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
<a href={t("grafana.promHref")} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline inline-flex items-center gap-1">
Prometheus<ExternalLink className="w-5 h-5" />
</a>{" "}+{" "}
<a href={t("grafana.grafanaHref")} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline inline-flex items-center gap-1">
Grafana<ExternalLink className="w-5 h-5" />
</a>
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("grafana.intro", { code })}
</p>
<figure className="my-6">
<Image src="/monitor/integrations/grafana.png" alt={t("grafana.imageAlt")} width={2000} height={1327} className="rounded-lg border border-gray-200 shadow-sm w-full h-auto mx-auto max-w-2xl" />
<figcaption className="text-sm text-gray-500 mt-2 text-center italic">{t.rich("grafana.imageCaption", { link: promAnchor })}</figcaption>
</figure>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("grafana.step1Title")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("grafana.step1Body", { code })}
</p>
<CopyableCode
code={`# /etc/prometheus/prometheus.yml
scrape_configs:
- job_name: 'proxmenux'
metrics_path: /api/prometheus
scheme: https # http if TLS isn't enabled in the Monitor
scrape_interval: 30s
authorization:
type: Bearer
credentials: 'your_actual_api_token_here'
static_configs:
- targets:
- 'pve01.lan:8008'
- 'pve02.lan:8008'
- 'pve03.lan:8008'`}
className="my-4"
/>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("grafana.step1After", { code, em })}
</p>
<Callout variant="tip" title={t("grafana.tokenTipTitle")}>
{t.rich("grafana.tokenTipBody", { code })}
</Callout>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("grafana.step2Title")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("grafana.step2Body", { code, 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("grafana.headerQuery")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("grafana.headerConfirms")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{verifyRows.map((row, idx) => (
<tr key={row.query} className={idx < verifyRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top font-mono text-xs">{row.query}</td>
<td className="px-3 py-2 align-top">{t.rich(`grafana.verifyRows.${idx}.confirms`, { code })}</td>
</tr>
))}
</tbody>
</table>
</div>
<Callout variant="info" title={t("grafana.calloutTitle")}>
{t.rich("grafana.calloutBody", { em, code })}
</Callout>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("grafana.step3Title")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("grafana.step3Body", { em, code })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("grafana.step4Title")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">{t("grafana.step4Body")}</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("grafana.headerPanel")}</th>
<th className="text-left px-3 py-2 border-b border-gray-200">{t("grafana.headerPromql")}</th>
</tr>
</thead>
<tbody className="text-gray-800">
{panelRows.map((row, idx) => (
<tr key={row.panel} className={idx < panelRows.length - 1 ? "border-b border-gray-100" : ""}>
<td className="px-3 py-2 align-top">{row.panel}</td>
<td className="px-3 py-2 align-top font-mono text-xs">{row.promql}</td>
</tr>
))}
</tbody>
</table>
</div>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("grafana.outro", { em, code })}
</p>
<h2 id="uptime-kuma" className="text-2xl font-semibold mt-10 mb-4 text-gray-900">
<a href={t("uptimeKuma.href")} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline inline-flex items-center gap-1">
Uptime Kuma<ExternalLink className="w-5 h-5" />
</a>{" "}and other status checkers
</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("uptimeKuma.intro", { code })}
</p>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("uptimeKuma.kumaTitle")}</h3>
<ol className="list-decimal pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{kumaSteps.map((_, idx) => (
<li key={idx}>{t.rich(`uptimeKuma.kumaSteps.${idx}`, { em, code })}</li>
))}
</ol>
<CopyableCode
code={`# Verify the response shape — no token required
curl http://pve01.lan:8008/api/system-info | jq
# → { "hostname": "...", "uptime": "...", "health": { "status": "healthy", ... }, ... }
# A failing scriptable check (non-zero when the Monitor isn't reporting healthy)
curl -s http://pve01.lan:8008/api/system-info | jq -e '.health.status == "healthy"'`}
className="my-4"
/>
<h3 className="text-lg font-semibold mt-6 mb-2 text-gray-900">{t("uptimeKuma.healthchecksTitle")}</h3>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("uptimeKuma.healthchecksBody", { code })}
</p>
<Callout variant="info" title={t("uptimeKuma.richTitle")}>
{t.rich("uptimeKuma.richBody", { code })}
</Callout>
<h2 id="custom-workflows" className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("workflows.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("workflows.intro", { em, code })}
</p>
<CopyableCode
code={`# A nightly cron job reports it ran longer than its threshold
curl -X POST http://proxmox.example.tld:8008/api/notifications/send \\
-H "Authorization: Bearer your_actual_api_token_here" \\
-H "Content-Type: application/json" \\
-d '{
"event_type":"system_problem",
"severity":"WARNING",
"title":"Nightly rsync took 4h27m",
"body":"The nightly-rsync job exceeded the 2h alert threshold. Check the source storage.",
"data":{
"service_name":"nightly-rsync",
"hostname":"backup-host"
}
}'`}
className="my-4"
/>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("workflows.n8nBody", { em, code })}
</p>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("workflows.severityBody", { code, link: notifEventsLink })}
</p>
<h2 id="pve-webhook" className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("pveWebhook.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("pveWebhook.intro1", { em })}
</p>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("pveWebhook.intro2", { code, link: pveLink })}
</p>
<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>
)
}