New version 1.2.5

Stable release consolidating the v1.2.4 beta cycle (1.2.4.1-beta and 1.2.4.2-beta) into 1.2.5.

Highlights:

- Apps dashboard: single launcher for every LXC-registered app and user-defined Custom Web Link, with category badges, search, sort and one-click deep-links back to the guest modal.
- LXC Apps & Updates end-to-end: App tab inside every guest modal, upstream version tracking, and Easy Updates that cover OS packages, registered apps, Docker Engine and per-image updates on the same 24-hour cycle.
- Application detection catalog with 380+ tracked workloads generated live from community-scripts across seven detector methods.
- Monitor now speaks 8 languages: English, Spanish, German, French, Italian, Portuguese, Slovak and Swedish (i18n scaffolding by @vaso73).
- NVIDIA multi-GPU passthrough by exact BDF so one card can be assigned to a VM while another stays operational on the host or LXC.
- Navigation reorder, Memory & Swap real memory-pressure signal, native Pushover channel, Actions API, plus wide-reaching improvements across health, hardware, network, backup and post-install.

Full release notes: see CHANGELOG.md and https://github.com/MacRimi/ProxMenux/releases
This commit is contained in:
MacRimi
2026-09-01 19:15:42 +02:00
parent f8e65cc4c4
commit 315259f5ec
54 changed files with 4343 additions and 333 deletions
@@ -0,0 +1,181 @@
import type { Metadata } from "next"
import type React from "react"
import { getMessages, getTranslations, setRequestLocale } from "next-intl/server"
import { Link } from "@/i18n/navigation"
import { Callout } from "@/components/ui/callout"
import { DocHeader } from "@/components/ui/doc-header"
export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string }>
}): Promise<Metadata> {
const { locale } = await params
const t = await getTranslations({ locale, namespace: "docs.monitor.dashboard.apps.meta" })
return { title: t("title"), description: t("description") }
}
type SourceRow = { source: string; appears: string; managedFrom: string }
type FieldRow = { field: string; required: string; purpose: string }
type ProblemRow = { problem: string; resolution: string }
export default async function AppsDashboardPage({
params,
}: {
params: Promise<{ locale: string }>
}) {
const { locale } = await params
setRequestLocale(locale)
const t = await getTranslations({ locale, namespace: "docs.monitor.dashboard.apps" })
const messages = (await getMessages({ locale })) as unknown as {
docs: { monitor: { dashboard: { apps: {
sources: { rows: SourceRow[] }
cards: { items: string[] }
toolbar: { items: string[] }
customLinks: { steps: string[]; fields: FieldRow[] }
categories: { items: string[] }
persistence: { items: string[] }
troubleshooting: { rows: ProblemRow[] }
whereNext: { items: { label: string; href: string; tail: string }[] }
} } } }
}
const a = messages.docs.monitor.dashboard.apps
const strong = (chunks: React.ReactNode) => <strong>{chunks}</strong>
const em = (chunks: React.ReactNode) => <em>{chunks}</em>
const code = (chunks: React.ReactNode) => <code className="rounded bg-gray-100 px-1 text-sm">{chunks}</code>
const appTabLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/dashboard/vms-lxcs/app" className="text-blue-600 hover:underline">{chunks}</Link>
)
const updatesLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/dashboard/vms-lxcs/updates" className="text-blue-600 hover:underline">{chunks}</Link>
)
const settingsLink = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/dashboard/settings#navigation-order" className="text-blue-600 hover:underline">{chunks}</Link>
)
const richList = (base: string, items: string[]) => (
<ul className="mt-2 list-disc space-y-2 pl-6 text-gray-800">
{items.map((_, idx) => (
<li key={idx}>{t.rich(`${base}.${idx}`, { strong, em, code, appTabLink, updatesLink, settingsLink })}</li>
))}
</ul>
)
return (
<div className="mx-auto max-w-4xl px-4 py-8">
<DocHeader
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={6}
/>
<Callout variant="info" title={t("intro.title")}>
{t.rich("intro.body", { strong, appTabLink })}
</Callout>
<h2 className="mt-10 mb-4 text-2xl font-semibold text-gray-900">{t("sources.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">{t("sources.intro")}</p>
<div className="my-4 overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-300 px-3 py-2 text-left">{t("sources.colSource")}</th>
<th className="border border-gray-300 px-3 py-2 text-left">{t("sources.colAppears")}</th>
<th className="border border-gray-300 px-3 py-2 text-left">{t("sources.colManagedFrom")}</th>
</tr>
</thead>
<tbody>
{a.sources.rows.map((row) => (
<tr key={row.source}>
<td className="border border-gray-300 px-3 py-2 font-medium">{row.source}</td>
<td className="border border-gray-300 px-3 py-2">{row.appears}</td>
<td className="border border-gray-300 px-3 py-2">{row.managedFrom}</td>
</tr>
))}
</tbody>
</table>
</div>
<Callout variant="tip" title={t("sources.relationshipTitle")}>
{t.rich("sources.relationshipBody", { strong, appTabLink })}
</Callout>
<h2 className="mt-10 mb-4 text-2xl font-semibold text-gray-900">{t("cards.heading")}</h2>
<p className="text-gray-800">{t("cards.intro")}</p>
{richList("cards.items", a.cards.items)}
<h2 className="mt-10 mb-4 text-2xl font-semibold text-gray-900">{t("toolbar.heading")}</h2>
<p className="text-gray-800">{t("toolbar.intro")}</p>
{richList("toolbar.items", a.toolbar.items)}
<h2 className="mt-10 mb-4 text-2xl font-semibold text-gray-900">{t("customLinks.heading")}</h2>
<p className="text-gray-800">{t("customLinks.intro")}</p>
<ol className="mt-2 list-decimal space-y-2 pl-6 text-gray-800">
{a.customLinks.steps.map((_, idx) => (
<li key={idx}>{t.rich(`customLinks.steps.${idx}`, { strong, em, code })}</li>
))}
</ol>
<div className="my-6 overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-300 px-3 py-2 text-left">{t("customLinks.colField")}</th>
<th className="border border-gray-300 px-3 py-2 text-left">{t("customLinks.colRequired")}</th>
<th className="border border-gray-300 px-3 py-2 text-left">{t("customLinks.colPurpose")}</th>
</tr>
</thead>
<tbody>
{a.customLinks.fields.map((row) => (
<tr key={row.field}>
<td className="border border-gray-300 px-3 py-2 font-medium">{row.field}</td>
<td className="border border-gray-300 px-3 py-2">{row.required}</td>
<td className="border border-gray-300 px-3 py-2">{row.purpose}</td>
</tr>
))}
</tbody>
</table>
</div>
<Callout variant="warning" title={t("customLinks.editTitle")}>
{t.rich("customLinks.editBody", { strong, appTabLink })}
</Callout>
<h2 className="mt-10 mb-4 text-2xl font-semibold text-gray-900">{t("categories.heading")}</h2>
<p className="text-gray-800">{t("categories.intro")}</p>
{richList("categories.items", a.categories.items)}
<h2 className="mt-10 mb-4 text-2xl font-semibold text-gray-900">{t("persistence.heading")}</h2>
<p className="text-gray-800">{t("persistence.intro")}</p>
{richList("persistence.items", a.persistence.items)}
<h2 className="mt-10 mb-4 text-2xl font-semibold text-gray-900">{t("troubleshooting.heading")}</h2>
<div className="my-4 overflow-x-auto">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="bg-gray-100">
<th className="border border-gray-300 px-3 py-2 text-left">{t("troubleshooting.colProblem")}</th>
<th className="border border-gray-300 px-3 py-2 text-left">{t("troubleshooting.colResolution")}</th>
</tr>
</thead>
<tbody>
{a.troubleshooting.rows.map((row) => (
<tr key={row.problem}>
<td className="border border-gray-300 px-3 py-2 font-medium">{row.problem}</td>
<td className="border border-gray-300 px-3 py-2">{row.resolution}</td>
</tr>
))}
</tbody>
</table>
</div>
<h2 className="mt-10 mb-4 text-2xl font-semibold text-gray-900">{t("whereNext.heading")}</h2>
<ul className="list-disc space-y-1 pl-6 text-gray-800">
{a.whereNext.items.map((item) => (
<li key={item.href}>
<Link href={item.href} className="text-blue-600 hover:underline">{item.label}</Link>
{item.tail}
</li>
))}
</ul>
</div>
)
}
@@ -55,7 +55,7 @@ export default async function DashboardIndexPage({
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={3}
estimatedMinutes={4}
/>
<Callout variant="info" title={t("oneHeader.title")}>
@@ -31,6 +31,8 @@ export default async function SettingsTabPage({
const messages = (await getMessages({ locale })) as unknown as {
docs: { monitor: { dashboard: { settings: {
interfaceLanguage: { items: string[] }
navigationOrder: { items: string[] }
health: { items: string[]; activeItems: string[] }
thresholds: {
whatForItems: string[]
@@ -47,6 +49,8 @@ export default async function SettingsTabPage({
} } } }
}
const s = messages.docs.monitor.dashboard.settings
const interfaceLanguageItems = s.interfaceLanguage.items
const navigationOrderItems = s.navigationOrder.items
const healthItems = s.health.items
const activeSuppressionItems = s.health.activeItems
const whatForItems = s.thresholds.whatForItems
@@ -99,13 +103,39 @@ export default async function SettingsTabPage({
title={t("header.title")}
description={t("header.description")}
section={t("header.section")}
estimatedMinutes={9}
estimatedMinutes={10}
/>
<Callout variant="info" title={t("intro.title")}>
{t("intro.body")}
</Callout>
<h2 id="interface-language" className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("interfaceLanguage.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("interfaceLanguage.intro", { strong, code })}
</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{interfaceLanguageItems.map((_, idx) => (
<li key={idx}>{t.rich(`interfaceLanguage.items.${idx}`, { strong, code })}</li>
))}
</ul>
<Callout variant="tip" title={t("interfaceLanguage.scopeTitle")}>
{t.rich("interfaceLanguage.scopeBody", { strong, code })}
</Callout>
<h2 id="navigation-order" className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("navigationOrder.heading")}</h2>
<p className="mb-4 text-gray-800 leading-relaxed">
{t.rich("navigationOrder.intro", { strong, code })}
</p>
<ul className="list-disc pl-6 mb-4 text-gray-800 leading-relaxed space-y-1">
{navigationOrderItems.map((_, idx) => (
<li key={idx}>{t.rich(`navigationOrder.items.${idx}`, { strong, code })}</li>
))}
</ul>
<Callout variant="info" title={t("navigationOrder.landingTitle")}>
{t.rich("navigationOrder.landingBody", { strong, code })}
</Callout>
<h2 className="text-2xl font-semibold mt-10 mb-4 text-gray-900">{t("networkUnits.heading")}</h2>
<figure className="my-4">
@@ -62,6 +62,11 @@ export default async function AppTabPage({
{chunks}
</Link>
)
const linkApps = (chunks: React.ReactNode) => (
<Link href="/docs/monitor/dashboard/apps" className="text-blue-600 hover:underline">
{chunks}
</Link>
)
const richList = (base: string, items: string[]) => (
<ul className="mt-2 list-disc space-y-2 pl-6 text-gray-800">
{items.map((_, idx) => (
@@ -77,6 +82,9 @@ export default async function AppTabPage({
<p className="mt-6 text-gray-800">{t.rich("intro.p1", { strong, em, code })}</p>
<p className="mt-4 text-gray-800">{t.rich("intro.p2", { strong, em, code, link: linkUpdates })}</p>
<Callout variant="info">{t.rich("intro.callout", { strong, em, code })}</Callout>
<Callout variant="tip" title={t("intro.dashboardTitle")}>
{t.rich("intro.dashboard", { strong, appsLink: linkApps })}
</Callout>
<h2 className="mt-10 mb-4 text-2xl font-semibold text-gray-900">{t("overview.heading")}</h2>
<p className="text-gray-800">{t("overview.lead")}</p>