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 { DataFlowDiagram } from "@/components/ui/data-flow-diagram" import CopyableCode from "@/components/CopyableCode" export async function generateMetadata({ params, }: { params: Promise<{ locale: string }> }): Promise { const { locale } = await params const t = await getTranslations({ locale, namespace: "docs.monitor.notifications.meta" }) return { title: t("title"), description: t("description"), keywords: [ "proxmox notifications", "proxmox telegram", "proxmox discord", "proxmox email alerts", "proxmox gotify", "proxmox apprise", "proxmox ntfy", "proxmox matrix notifications", "proxmox alerts", "proxmox notification webhook", ], alternates: { canonical: "https://proxmenux.com/docs/monitor/notifications" }, openGraph: { title: t("ogTitle"), description: t("ogDescription"), type: "article", url: "https://proxmenux.com/docs/monitor/notifications", }, twitter: { card: "summary_large_image", title: t("twitterTitle"), description: t("twitterDescription"), }, } } type SourceRow = { collector: string; watches: string; events: string } type DispatchRow = { stage: string; what: string; tunable: string } type CatalogueRow = { group: string; events: string } type ApiRow = { endpoint: string; method: string; use: string } type WhereNextItem = { label: string; href: string; tail?: string; tailRich?: string } export default async function NotificationsPage({ params, }: { params: Promise<{ locale: string }> }) { const { locale } = await params setRequestLocale(locale) const t = await getTranslations({ locale, namespace: "docs.monitor.notifications" }) const messages = (await getMessages({ locale })) as unknown as { docs: { monitor: { notifications: { enabling: { steps: string[] } sources: { rows: SourceRow[] } telegram: { step1Items: string[] privateItems: string[] groupItems: string[] } discord: { items: string[] } gotify: { items: string[] } email: { gmailItems: string[]; outlookItems: string[] } apprise: { listItems: string[]; steps: string[] } rich: { togglesItems: string[] } quiet: { purposeItems: string[]; howItems: string[] } digest: { purposeItems: string[] howItems: string[] neverDelayedSub: string[] } dispatch: { rows: DispatchRow[] } pveWebhook: { registeredItems: string[] securityItems: string[] actionsItems: string[] } catalogue: { rows: CatalogueRow[] } api: { rows: ApiRow[] } whereNext: { items: WhereNextItem[] } } } } } const n = messages.docs.monitor.notifications const enablingSteps = n.enabling.steps const sourceRows = n.sources.rows const tgStep1 = n.telegram.step1Items const tgPriv = n.telegram.privateItems const tgGroup = n.telegram.groupItems const discordItems = n.discord.items const gotifyItems = n.gotify.items const gmailItems = n.email.gmailItems const outlookItems = n.email.outlookItems const appriseListItems = n.apprise.listItems const appriseSteps = n.apprise.steps const togglesItems = n.rich.togglesItems const quietPurpose = n.quiet.purposeItems const quietHow = n.quiet.howItems const digestPurpose = n.digest.purposeItems const digestHow = n.digest.howItems const digestNeverSub = n.digest.neverDelayedSub const dispatchRows = n.dispatch.rows const pveRegistered = n.pveWebhook.registeredItems const pveSecurity = n.pveWebhook.securityItems const pveActions = n.pveWebhook.actionsItems const catalogueRows = n.catalogue.rows const apiRows = n.api.rows const whereNextItems = n.whereNext.items const code = (chunks: React.ReactNode) => {chunks} const strong = (chunks: React.ReactNode) => {chunks} const em = (chunks: React.ReactNode) => {chunks} const hmLink = (chunks: React.ReactNode) => ( {chunks} ) const pveLink = (chunks: React.ReactNode) => ( {chunks} ) const aiLink = (chunks: React.ReactNode) => ( {chunks} ) const aiPageLink = (chunks: React.ReactNode) => ( {chunks} ) const catalogueLink = (chunks: React.ReactNode) => ( {chunks} ) const quietLink = (chunks: React.ReactNode) => ( {chunks} ) const ext = (href: string) => (chunks: React.ReactNode) => ( {chunks} ) return (
{t.rich("intro.body", { link: hmLink })}

{t("howItWorks.heading")}

{t("howItWorks.intro")}

{t("enabling.heading")}

{t.rich("enabling.intro", { em })}

{t("enabling.disabledAlt")}
{t("enabling.disabledCaption")}

{t("enabling.stepsIntro")}

    {enablingSteps.map((_, idx) => (
  1. {t.rich(`enabling.steps.${idx}`, { em, code, pvelink: pveLink })}
  2. ))}
{t("enabling.activeAlt")}
{t.rich("enabling.activeCaption", { em })}

{t("sources.heading")}

{t.rich("sources.intro", { code })}

{sourceRows.map((row, idx) => ( ))}
{t("sources.headerCollector")} {t("sources.headerWatches")} {t("sources.headerEvents")}
{row.collector} {t.rich(`sources.rows.${idx}.watches`, { code, pvelink: pveLink })} {t.rich(`sources.rows.${idx}.events`, { code })}

{t.rich("sources.after1", { code })}

{t.rich("sources.after2", { code, ailink: aiLink })}

{t("channels.heading")}

{t.rich("channels.intro", { em })}

{t.rich("channels.credsBody", { code })}

{t("telegram.heading")}

{t.rich("telegram.intro", { strong, em })}

{t("telegram.guideAlt")}
{t.rich("telegram.guideCaption", { em })}

{t("telegram.step1Title")}

    {tgStep1.map((_, idx) => (
  1. {t.rich(`telegram.step1Items.${idx}`, { em, code, a: ext("https://t.me/BotFather") })}
  2. ))}

{t("telegram.step2Title")}

{t.rich("telegram.step2Intro", { em })}

{t("telegram.privateLabel")}

    {tgPriv.map((_, idx) => (
  1. {t.rich(`telegram.privateItems.${idx}`, { code, a1: ext("https://t.me/userinfobot"), a2: ext("https://t.me/myidbot"), a: ext("https://t.me/userinfobot"), })}
  2. ))}
{t("telegram.privateAlt")}
{t("telegram.privateCaption")}

{t("telegram.groupLabel")}

    {tgGroup.map((_, idx) => (
  1. {t.rich(`telegram.groupItems.${idx}`, { code, em })}
  2. ))}
{t("telegram.groupAlt")}
{t.rich("telegram.groupCaption", { code, em })}

{t("telegram.step3Title")}

{t.rich("telegram.step3Body", { em })}

{t("discord.heading")}

{t.rich("discord.intro", { em })}

    {discordItems.map((_, idx) => (
  1. {t.rich(`discord.items.${idx}`, { em, code })}
  2. ))}
{t("discord.imageAlt")}
{t.rich("discord.imageCaption", { em })}

{t("gotify.heading")}

{t.rich("gotify.intro", { em })}

    {gotifyItems.map((_, idx) => (
  1. {t.rich(`gotify.items.${idx}`, { em, code, a: ext("https://gotify.net/docs/install") })}
  2. ))}
{t("gotify.imageAlt")}
{t("gotify.imageCaption")}

{t("email.heading")}

{t("email.intro")}

{t("email.imageAlt")}
{t("email.imageCaption")}

{t.rich("email.appNote", { strong })}

{t("email.gmailTitle")}

{t.rich("email.gmailIntro", { strong, em })}

    {gmailItems.map((_, idx) => (
  1. {t.rich(`email.gmailItems.${idx}`, { em, code, a: idx === 0 ? ext("https://myaccount.google.com/security") : ext("https://myaccount.google.com/apppasswords"), })}
  2. ))}

{t("email.outlookTitle")}

{t.rich("email.outlookIntro", { strong })}

    {outlookItems.map((_, idx) => (
  1. {t.rich(`email.outlookItems.${idx}`, { em, code, a: ext("https://account.microsoft.com/security") })}
  2. ))}
{t("email.relayBody")}

{t("apprise.heading")}

{t("apprise.intro")}

{t("apprise.listIntro")}

    {appriseListItems.map((_, idx) => (
  • {t.rich(`apprise.listItems.${idx}`, { a: idx === 0 ? ext("https://github.com/caronc/apprise/wiki") : ext("https://github.com/caronc/apprise/wiki/URLBasics"), })}
  • ))}

{t("apprise.stepsTitle")}

    {appriseSteps.map((_, idx) => (
  1. {t.rich(`apprise.steps.${idx}`, { em, code, a: ext("https://github.com/caronc/apprise/wiki") })}
  2. ))}
{t("apprise.deliveredBody")} {t.rich("apprise.fanoutBody", { a: ext("https://github.com/caronc/apprise-api") })}

{t("rich.heading")}

{t.rich("rich.intro", { em })}

{t("rich.imageAlt")}
{t.rich("rich.imageCaption", { em })}

{t("rich.richTitle")}

{t.rich("rich.richIntro", { em })}

{t("rich.plainHeader")}
{`[INFO] vm_start
VM 101 (homeassistant) started
on node pve-01
host: home-lab`}
          
{t("rich.richHeader")}
{`🟢 VM started
VM 101 (homeassistant) is now
running on node pve-01
🏠 home-lab`}
          

{t("rich.richOutro")}

{t("rich.togglesTitle")}

{t("rich.togglesIntro")}

    {togglesItems.map((_, idx) => (
  1. {t.rich(`rich.togglesItems.${idx}`, { strong, em, code })}
  2. ))}

{t.rich("rich.togglesOutro", { em, code })}

{t("quiet.heading")}

{t.rich("quiet.intro", { strong })}

{t("quiet.imageAlt")}
{t("quiet.imageCaption")}

{t("quiet.purposeTitle")}

    {quietPurpose.map((_, idx) => (
  • {t.rich(`quiet.purposeItems.${idx}`, { strong })}
  • ))}

{t("quiet.howTitle")}

    {quietHow.map((_, idx) => (
  1. {t.rich(`quiet.howItems.${idx}`, { strong, code })}
  2. ))}
{t.rich("quiet.criticalBody", { link: catalogueLink })}

{t("digest.heading")}

{t.rich("digest.intro1", { strong })}

{t.rich("digest.intro2", { link: quietLink })}

{t("digest.purposeTitle")}

    {digestPurpose.map((_, idx) => (
  • {t.rich(`digest.purposeItems.${idx}`, { strong })}
  • ))}

{t("digest.howTitle")}

    {digestHow.map((_, idx) => (
  1. {t.rich(`digest.howItems.${idx}`, { strong, em, code })} {idx === 3 && (
      {digestNeverSub.map((_, sIdx) => (
    • {t.rich(`digest.neverDelayedSub.${sIdx}`, { strong })}
    • ))}
    )}
  2. ))}
{t.rich("digest.comboBody", { em })}

{t("displayName.heading")}

{t.rich("displayName.intro", { em, code })}

{t("displayName.imageAlt")}
{t("displayName.imageCaption")}

{t.rich("displayName.outro", { em, code })}

{t("dispatch.heading")}

{t("dispatch.intro")}

{dispatchRows.map((row, idx) => ( ))}
{t("dispatch.headerStage")} {t("dispatch.headerWhat")} {t("dispatch.headerTunable")}
{row.stage} {t.rich(`dispatch.rows.${idx}.what`, { code })} {row.tunable}
{t("dispatch.calloutBody")}

{t("aiRewrite.heading")}

{t("aiRewrite.body1")}

{t.rich("aiRewrite.body2", { code, link: aiPageLink })}

{t("aiRewrite.privacyBody")}

{t("pveWebhook.heading")}

{t.rich("pveWebhook.intro1", { em, code })}

{t.rich("pveWebhook.intro2", { em })}

{t("pveWebhook.imageAlt")}
{t("pveWebhook.imageCaption")}

{t("pveWebhook.registeredIntro")}

    {pveRegistered.map((_, idx) => (
  • {t.rich(`pveWebhook.registeredItems.${idx}`, { strong, em, code })} {idx === 1 && ( {`{ "title": "{{ escape title }}", "message": "{{ escape message }}", "severity": "{{ severity }}", "timestamp": "{{ timestamp }}", "fields": {{ json fields }} }`} )}
  • ))}

{t("pveWebhook.securityTitle")}

{t.rich("pveWebhook.securityIntro", { code })}

    {pveSecurity.map((_, idx) => (
  • {t.rich(`pveWebhook.securityItems.${idx}`, { strong, code })}
  • ))}
{t.rich("pveWebhook.practiceBody", { code })}

{t("pveWebhook.actionsIntro")}

    {pveActions.map((_, idx) => (
  • {t.rich(`pveWebhook.actionsItems.${idx}`, { strong, code })}
  • ))}
{t.rich("pveWebhook.clusterBody", { code, em })}

{t("catalogue.heading")}

{t("catalogue.intro")}

{catalogueRows.map((row, idx) => ( ))}
{t("catalogue.headerGroup")} {t("catalogue.headerEvents")}
{row.group} {t.rich(`catalogue.rows.${idx}.events`, { code })}

{t.rich("catalogue.burstNote", { code })}

{t("history.heading")}

{t.rich("history.body1", { em, code })}

{t.rich("history.body2", { em })}

{t.rich("history.body3", { code })}

{t("api.heading")}

{apiRows.map((row, idx) => ( ))}
{t("api.headerEndpoint")} {t("api.headerMethod")} {t("api.headerUse")}
{row.endpoint} {row.method} {t.rich(`api.rows.${idx}.use`, { code })}
:8008/api/notifications/test \\ -H "Authorization: Bearer " \\ -H "Content-Type: application/json" \\ -d '{"channel":"discord"}' # Emit a custom event from a script curl -X POST http://:8008/api/notifications/send \\ -H "Authorization: Bearer " \\ -H "Content-Type: application/json" \\ -d '{"event_type":"custom","severity":"warning","data":{"message":"Cron job took >10 min"}}' # Pull the last 50 history entries for one channel curl -H "Authorization: Bearer " \\ 'http://:8008/api/notifications/history?channel=telegram&limit=50' | jq # Test an AI provider connection (verifies the API key and model) curl -X POST http://:8008/api/notifications/test-ai \\ -H "Authorization: Bearer " \\ -H "Content-Type: application/json" \\ -d '{"provider":"openai","api_key":"sk-...","model":"gpt-4o-mini"}'`} className="my-4" />

{t("whereNext.heading")}

    {whereNextItems.map((item, idx) => (
  • {item.label} {item.tailRich ? t.rich(`whereNext.items.${idx}.tailRich`, { code }) : item.tail}
  • ))}
) }