feat: PBS Tab — Stat-Karten (Datastores/Backups/SyncJobs/Kernel) + neues Layout

This commit is contained in:
cynfo3000 2026-03-06 23:36:48 +01:00
parent 67651a5aeb
commit 1204e05d0b
2 changed files with 64 additions and 28 deletions

View File

@ -13,6 +13,7 @@ type PBSInfo struct {
Datastores []PBSDatastore `json:"datastores,omitempty"`
Tasks []PBSTask `json:"tasks,omitempty"`
GC []PBSGCStatus `json:"gc,omitempty"`
SyncJobs int `json:"sync_jobs"`
}
type PBSDatastore struct {
@ -161,6 +162,14 @@ func CollectPBS() *PBSInfo {
pbs.GC = append(pbs.GC, gc)
}
log.Printf("PBS: %d Datastores, %d Tasks gesammelt", len(pbs.Datastores), len(pbs.Tasks))
// Sync Jobs zählen
if out, err := exec.Command("/usr/sbin/proxmox-backup-manager", "sync-job", "list", "--output-format", "json").Output(); err == nil {
var jobs []interface{}
if json.Unmarshal(out, &jobs) == nil {
pbs.SyncJobs = len(jobs)
}
}
log.Printf("PBS: %d Datastores, %d Tasks, %d SyncJobs gesammelt", len(pbs.Datastores), len(pbs.Tasks), pbs.SyncJobs)
return pbs
}

View File

@ -127,7 +127,7 @@ export default function ProxmoxPanel({ agentId, customers, onClose, onReload })
) : tab === 'updates' ? (
<UpdatesTab sys={sys} />
) : tab === 'pbs' ? (
<PBSTab pbs={pbs} agentId={agentId} />
<PBSTab pbs={pbs} agentId={agentId} sys={sys} />
) : tab === 'backups' ? (
<BackupsTab sys={sys} />
) : tab === 'agent' ? (
@ -953,7 +953,7 @@ function formatUptime(seconds) {
return `${h}h ${m}m`
}
function PBSTab({ pbs, agentId }) {
function PBSTab({ pbs, agentId, sys }) {
const [loading, setLoading] = React.useState({})
const [feedback, setFeedback] = React.useState({})
@ -974,7 +974,6 @@ function PBSTab({ pbs, agentId }) {
if (s < 3600) return `${Math.floor(s/60)}m ${s%60}s`
return `${Math.floor(s/3600)}h ${Math.floor((s%3600)/60)}m`
}
const taskStatusColor = (s) => {
if (!s) return 'text-gray-500'
if (s === 'OK' || s.startsWith('OK')) return 'text-green-400'
@ -1003,18 +1002,57 @@ function PBSTab({ pbs, agentId }) {
}
}
const totalBackups = pbs.datastores?.reduce((s, d) => s + (d.backup_count || 0), 0) || 0
const kernel = sys?.opnsense_version || '—'
const StatCard = ({ icon, value, label, iconColor }) => (
<div className="flex-1 bg-[#1a2235] rounded-xl border border-gray-700/40 p-4 flex flex-col items-center gap-2">
<div className={`text-2xl ${iconColor}`}>{icon}</div>
<div className="text-2xl font-bold text-white">{value}</div>
<div className="text-xs text-gray-400">{label}</div>
</div>
)
return (
<div className="space-y-6">
{/* Header */}
{/* Titel */}
<div>
<div className="text-white font-semibold">Proxmox Backup Server</div>
{pbs.version && <div className="text-xs text-gray-500 mt-0.5">Version {pbs.version}</div>}
<div className="text-lg font-semibold text-white">Backup Server</div>
{pbs.version && <div className="text-xs text-gray-500 mt-0.5">PBS {pbs.version}</div>}
</div>
{/* Stat-Karten */}
<div className="flex gap-3">
<StatCard
icon={<svg className="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><ellipse cx="12" cy="5" rx="9" ry="3" strokeWidth="2"/><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M3 5v14a9 3 0 0018 0V5M3 12a9 3 0 0018 0"/></svg>}
value={pbs.datastores?.length || 0}
label="Datastores"
iconColor="text-orange-400"
/>
<StatCard
icon={<svg className="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8"/></svg>}
value={totalBackups}
label="Backups"
iconColor="text-blue-400"
/>
<StatCard
icon={<svg className="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>}
value={pbs.sync_jobs ?? 0}
label="Sync Jobs"
iconColor="text-green-400"
/>
<StatCard
icon={<svg className="w-7 h-7" fill="none" stroke="currentColor" viewBox="0 0 24 24"><rect x="2" y="6" width="20" height="4" rx="1" strokeWidth="2"/><rect x="2" y="14" width="20" height="4" rx="1" strokeWidth="2"/><line x1="6" y1="8" x2="6" y2="8" strokeWidth="3" strokeLinecap="round"/><line x1="6" y1="16" x2="6" y2="16" strokeWidth="3" strokeLinecap="round"/></svg>}
value={kernel !== '—' ? kernel.split('-')[0] : '—'}
label="Kernel"
iconColor="text-orange-400"
/>
</div>
{/* Datastores */}
{pbs.datastores?.length > 0 && (
<div className="space-y-3">
<h3 className="text-sm font-medium text-gray-400">Datastores</h3>
<h3 className="text-sm font-semibold text-white">Datastores</h3>
{pbs.datastores.map((ds) => {
const usedPct = ds.total > 0 ? (ds.used / ds.total * 100) : 0
const barColor = usedPct > 90 ? 'bg-red-500' : usedPct > 70 ? 'bg-yellow-500' : 'bg-green-500'
@ -1035,46 +1073,35 @@ function PBSTab({ pbs, agentId }) {
}
return (
<div key={ds.name} className="bg-[#1a2235] rounded-xl border border-gray-700/40 p-4 space-y-3">
{/* Titel + Backup-Count */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-orange-500/20 flex items-center justify-center">
<svg className="w-4 h-4 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
</svg>
</div>
<svg className="w-4 h-4 text-orange-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
</svg>
<span className="text-white font-semibold">{ds.name}</span>
</div>
{ds.backup_count > 0 && (
<span className="text-sm text-gray-400">{ds.backup_count} Backups</span>
)}
{ds.backup_count > 0 && <span className="text-sm text-gray-400">{ds.backup_count} Backups</span>}
</div>
{/* Speicher-Balken */}
<div>
<div className="flex justify-between text-xs text-gray-400 mb-1.5">
<span>{formatBytes(ds.used)} / {formatBytes(ds.total)}</span>
<span>{usedPct.toFixed(1)}%</span>
</div>
<div className="h-2 bg-gray-700 rounded-full overflow-hidden">
<div className={`h-full rounded-full ${barColor} transition-all`} style={{ width: `${Math.min(usedPct, 100)}%` }} />
<div className={`h-full rounded-full ${barColor}`} style={{ width: `${Math.min(usedPct, 100)}%` }} />
</div>
</div>
{/* GC-Status */}
{gc && gc.status && (
{gc?.status && (
<div className="text-xs text-gray-500 flex gap-3">
<span>GC: <span className={gc.status === 'ok' ? 'text-green-400' : gc.status === 'running' ? 'text-blue-400' : 'text-gray-500'}>{gc.status}</span></span>
{gc.last_run > 0 && <span>Letzter Lauf: {formatTs(gc.last_run)}</span>}
{gc.duration > 0 && <span>({formatDuration(gc.duration)})</span>}
</div>
)}
{/* Aktions-Buttons */}
<div className="flex gap-2 pt-1">
<ActionBtn action="gc" label="GC" cls="bg-yellow-500 hover:bg-yellow-400 text-gray-900" />
<ActionBtn action="gc" label="GC" cls="bg-yellow-600 hover:bg-yellow-500 text-white" />
<ActionBtn action="verify" label="Verify" cls="bg-gray-600 hover:bg-gray-500 text-white" />
<ActionBtn action="prune" label="Prune" cls="bg-orange-600 hover:bg-orange-500 text-white" />
<ActionBtn action="prune" label="Prune" cls="bg-orange-700 hover:bg-orange-600 text-white" />
</div>
</div>
)
@ -1085,7 +1112,7 @@ function PBSTab({ pbs, agentId }) {
{/* Tasks */}
{pbs.tasks?.length > 0 && (
<div className="space-y-2">
<h3 className="text-sm font-medium text-gray-400">Letzte Tasks</h3>
<h3 className="text-sm font-semibold text-white">Letzte Tasks</h3>
<div className="bg-gray-800/50 rounded-lg border border-gray-700/50 overflow-hidden">
<table className="w-full text-xs">
<thead>