feat: PBS Tab — Stat-Karten (Datastores/Backups/SyncJobs/Kernel) + neues Layout
This commit is contained in:
parent
67651a5aeb
commit
1204e05d0b
@ -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
|
||||
}
|
||||
|
||||
@ -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>
|
||||
<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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user