feat(pbs): post-install menu for Proxmox Backup Server

- Open the post-install menu from the PBS menu
- List only the customizable options that apply to PBS
- Offer the PBS no-subscription repository through the native repository API
- Upgrade PBS with the package list shown before applying it
- Remove and restore the subscription notice on the PBS web interface
- Install Figurine on PBS without the Proxmox VE aliases
- Offer the Helper-Scripts PBS post-install and microcode scripts
- Tune firewall bridges after the systemd udev rules in the automated post-install
- Add PBS repository policy tests and syntax checks
- Document the PBS differences on the post-install page
- Use backups in the Spanish PBS update message
This commit is contained in:
MacRimi
2026-10-17 18:13:44 +02:00
parent ce1a3de719
commit 98ae1f3626
34 changed files with 1976 additions and 227 deletions
+70 -10
View File
@@ -1,10 +1,12 @@
"use client"
import { useState, useEffect } from "react"
import useSWR from "swr"
import { Card, CardContent, CardHeader, CardTitle } from "./ui/card"
import { Progress } from "./ui/progress"
import { Badge } from "./ui/badge"
import { Cpu, MemoryStick, Thermometer, Server, Zap, AlertCircle, HardDrive, Network, ChevronRight } from "lucide-react"
import { Cpu, MemoryStick, Thermometer, Server, Zap, AlertCircle, HardDrive, Network, ChevronRight, Database } from "lucide-react"
import { DatastoreUsageRow, type PBSDatastore } from "./pbs-datastore-usage"
import { NodeMetricsCharts } from "./node-metrics-charts"
import { NetworkTrafficChart } from "./network-traffic-chart"
import { TemperatureDetailModal } from "./temperature-detail-modal"
@@ -185,9 +187,17 @@ const getUnitsSettings = (): "Bytes" | "Bits" => {
return raw && raw.toLowerCase() === "bits" ? "Bits" : "Bytes"
}
export function SystemOverview() {
export function SystemOverview({ product = "pve" }: { product?: "pve" | "pbs" } = {}) {
const t = useT()
const { language } = useI18n()
const isPbs = product === "pbs"
// On Proxmox Backup Server the datastores take the place of guests and PVE storage.
const { data: pbsStatus, error: pbsError } = useSWR<{ datastores: PBSDatastore[] | null }>(
isPbs ? "/api/pbs/status?server=local" : null,
(url: string) => fetchApi<{ datastores: PBSDatastore[] | null }>(url),
{ refreshInterval: 60000, revalidateOnFocus: false },
)
const datastores = pbsStatus?.datastores ?? null
const [systemData, setSystemData] = useState<SystemData | null>(null)
const [vmData, setVmData] = useState<VMData[]>([])
const [storageData, setStorageData] = useState<StorageData | null>(null)
@@ -212,8 +222,8 @@ export function SystemOverview() {
const fetchAllData = async () => {
const [systemResult, vmResult, storageResults, networkResult] = await Promise.all([
fetchSystemData().finally(() => setLoadingStates((prev) => ({ ...prev, system: false }))),
fetchVMData().finally(() => setLoadingStates((prev) => ({ ...prev, vms: false }))),
Promise.all([fetchStorageData(), fetchProxmoxStorageData()]).finally(() =>
(isPbs ? Promise.resolve([]) : fetchVMData()).finally(() => setLoadingStates((prev) => ({ ...prev, vms: false }))),
Promise.all([fetchStorageData(), isPbs ? Promise.resolve(null) : fetchProxmoxStorageData()]).finally(() =>
setLoadingStates((prev) => ({ ...prev, storage: false })),
),
fetchNetworkData().finally(() => setLoadingStates((prev) => ({ ...prev, network: false }))),
@@ -248,12 +258,13 @@ export function SystemOverview() {
}, 5000)
const vmInterval = setInterval(async () => {
if (isPbs) return
const data = await fetchVMData()
setVmData(data)
}, 59000)
const storageInterval = setInterval(async () => {
const [storage, proxmoxStorage] = await Promise.all([fetchStorageData(), fetchProxmoxStorageData()])
const [storage, proxmoxStorage] = await Promise.all([fetchStorageData(), isPbs ? Promise.resolve(null) : fetchProxmoxStorageData()])
if (storage) setStorageData(storage)
if (proxmoxStorage) setProxmoxStorageData(proxmoxStorage)
}, 59000)
@@ -278,7 +289,7 @@ export function SystemOverview() {
clearInterval(networkInterval)
window.removeEventListener("networkUnitChanged" as any, handleUnitChange)
}
}, [t])
}, [t, isPbs])
if (!hasAttemptedLoad || loadingStates.system) {
return (
@@ -527,7 +538,46 @@ export function SystemOverview() {
</CardContent>
</Card>
{/* ── Active VM & LXC (preview restyle v2: pills mismo tamaño que "X running") ── */}
{isPbs ? (
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">{t("pbs.datastores")}</CardTitle>
<Database className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
{!datastores && !pbsError ? (
<div className="space-y-2 animate-pulse">
<div className="h-10 bg-muted rounded w-20"></div>
<div className="h-5 bg-muted rounded w-32"></div>
</div>
) : !datastores ? (
<p className="text-sm text-amber-500">{t("pbs.loadFailed")}</p>
) : datastores.length === 0 ? (
<p className="text-sm text-muted-foreground">{t("pbs.noDatastores")}</p>
) : (
<div className="space-y-3">
<div className="flex items-end justify-between">
<span className="text-4xl font-bold leading-none text-foreground">{datastores.length}</span>
{datastores.some(store => store.error) ? (
<Badge variant="outline" className="bg-amber-500/10 text-amber-500 border-amber-500/20">
{t("pbs.datastoresUnavailable", { count: datastores.filter(store => store.error).length })}
</Badge>
) : (
<Badge variant="outline" className="bg-green-500/10 text-green-500 border-green-500/20">
{t("pbs.datastoresOnline")}
</Badge>
)}
</div>
{datastores.slice(0, 2).map(store => <DatastoreUsageRow key={store.name} store={store} compact />)}
{datastores.length > 2 && (
<p className="text-xs text-muted-foreground">{t("pbs.moreDatastores", { count: datastores.length - 2 })}</p>
)}
</div>
)}
</CardContent>
</Card>
) : (
/* ── Active VM & LXC (preview restyle v2: pills mismo tamaño que "X running") ── */
<Card className="bg-card border-border">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium text-muted-foreground">{t("overview.activeVmLxc")}</CardTitle>
@@ -565,6 +615,7 @@ export function SystemOverview() {
)}
</CardContent>
</Card>
)}
<Card
className={`bg-card border-border ${systemData.temperature > 0 ? "cursor-pointer hover:bg-white/5 transition-colors" : ""}`}
@@ -708,7 +759,16 @@ export function SystemOverview() {
</div>
</div>
{vmLxcStorages && vmLxcStorages.length > 0 ? (
{isPbs ? (
<div className="space-y-3">
<div className="text-xs font-medium text-muted-foreground mb-2">{t("pbs.datastores")}</div>
{!datastores ? (
<div className="text-center py-4 text-muted-foreground text-sm">{pbsError ? t("pbs.loadFailed") : t("pbs.loading")}</div>
) : datastores.length === 0 ? (
<div className="text-center py-4 text-muted-foreground text-sm">{t("pbs.noDatastores")}</div>
) : datastores.map(store => <DatastoreUsageRow key={store.name} store={store} />)}
</div>
) : vmLxcStorages && vmLxcStorages.length > 0 ? (
<div className="space-y-2 pb-3 border-b border-border">
<div className="text-xs font-medium text-muted-foreground mb-2">{t("overview.vmLxcStorage")}</div>
<div className="flex justify-between items-center">
@@ -744,7 +804,7 @@ export function SystemOverview() {
</div>
)}
{localStorage && (
{!isPbs && localStorage && (
<div className="space-y-2">
<div className="text-xs font-medium text-muted-foreground mb-2">{t("overview.localStorageSystem")}</div>
<div className="flex justify-between items-center">
@@ -899,7 +959,7 @@ export function SystemOverview() {
<span className="text-foreground">{formatSystemUptime(systemData.uptime)}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">{t("overview.proxmoxVersion")}</span>
<span className="text-muted-foreground">{t(isPbs ? "overview.pbsVersion" : "overview.proxmoxVersion")}</span>
<span className="text-foreground">{systemData.proxmox_version || t("app.notAvailable")}</span>
</div>
<div className="flex justify-between">