feat: PBS Datastores — Backup-Anzahl, neues Layout mit GC/Verify/Prune Buttons

This commit is contained in:
cynfo3000 2026-03-06 23:34:43 +01:00
parent a036849565
commit 67651a5aeb
2 changed files with 109 additions and 49 deletions

View File

@ -16,12 +16,12 @@ type PBSInfo struct {
}
type PBSDatastore struct {
Name string `json:"name"`
Path string `json:"path,omitempty"`
Total int64 `json:"total"`
Used int64 `json:"used"`
Available int64 `json:"available"`
Namespaces int `json:"namespaces,omitempty"`
Name string `json:"name"`
Path string `json:"path,omitempty"`
Total int64 `json:"total"`
Used int64 `json:"used"`
Available int64 `json:"available"`
BackupCount int `json:"backup_count"`
}
type PBSTask struct {
@ -87,6 +87,14 @@ func CollectPBS() *PBSInfo {
if avail, ok := d["avail"].(float64); ok {
ds.Available = int64(avail)
}
// Backup-Anzahl ermitteln
if snapOut, err := exec.Command("/usr/sbin/proxmox-backup-manager", "snapshot", "list", ds.Name, "--output-format", "json").Output(); err == nil {
var snaps []interface{}
if json.Unmarshal(snapOut, &snaps) == nil {
ds.BackupCount = len(snaps)
}
}
pbs.Datastores = append(pbs.Datastores, ds)
}
}

View File

@ -127,7 +127,7 @@ export default function ProxmoxPanel({ agentId, customers, onClose, onReload })
) : tab === 'updates' ? (
<UpdatesTab sys={sys} />
) : tab === 'pbs' ? (
<PBSTab pbs={pbs} />
<PBSTab pbs={pbs} agentId={agentId} />
) : tab === 'backups' ? (
<BackupsTab sys={sys} />
) : tab === 'agent' ? (
@ -952,7 +952,11 @@ function formatUptime(seconds) {
if (d > 0) return `${d}d ${h}h`
return `${h}h ${m}m`
}
function PBSTab({ pbs }) {
function PBSTab({ pbs, agentId }) {
const [loading, setLoading] = React.useState({})
const [feedback, setFeedback] = React.useState({})
if (!pbs?.available) return <div className="p-4 text-gray-500">Keine PBS-Daten verfuegbar</div>
const formatBytes = (b) => {
@ -979,59 +983,109 @@ function PBSTab({ pbs }) {
return 'text-yellow-400'
}
const pbsAction = async (datastore, action) => {
const key = `${datastore}-${action}`
const cmds = {
gc: `/usr/sbin/proxmox-backup-manager garbage-collection start ${datastore}`,
verify: `/usr/sbin/proxmox-backup-manager verify ${datastore}`,
prune: `/usr/sbin/proxmox-backup-manager prune-datastore ${datastore}`,
}
setLoading(l => ({ ...l, [key]: true }))
setFeedback(f => ({ ...f, [key]: null }))
try {
await api.execCommand(agentId, cmds[action], 60)
setFeedback(f => ({ ...f, [key]: 'ok' }))
} catch (e) {
setFeedback(f => ({ ...f, [key]: 'err' }))
} finally {
setLoading(l => ({ ...l, [key]: false }))
setTimeout(() => setFeedback(f => ({ ...f, [key]: null })), 4000)
}
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center gap-3">
<div>
<div className="text-white font-semibold">Proxmox Backup Server</div>
{pbs.version && <div className="text-xs text-gray-500">Version {pbs.version}</div>}
</div>
<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>
{/* Datastores */}
{pbs.datastores?.length > 0 && (
<>
<h3 className="text-sm font-medium text-gray-300">Datastores</h3>
<div className="space-y-3">
{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'
const gc = pbs.gc?.find(g => g.datastore === ds.name)
<div className="space-y-3">
<h3 className="text-sm font-medium text-gray-400">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'
const gc = pbs.gc?.find(g => g.datastore === ds.name)
const mkKey = (a) => `${ds.name}-${a}`
const ActionBtn = ({ action, label, cls }) => {
const k = mkKey(action)
const fb = feedback[k]
return (
<div key={ds.name} className="bg-gray-800/50 rounded-lg border border-gray-700/50 p-4">
<div className="flex items-center justify-between mb-2">
<div>
<span className="text-white font-medium">{ds.name}</span>
{ds.path && <span className="text-xs text-gray-500 ml-2">{ds.path}</span>}
<button
onClick={() => pbsAction(ds.name, action)}
disabled={loading[k]}
className={`flex-1 py-1.5 rounded text-sm font-semibold transition-all ${cls} disabled:opacity-50`}
>
{loading[k] ? '...' : fb === 'ok' ? 'Gestartet' : fb === 'err' ? 'Fehler' : label}
</button>
)
}
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-sm text-gray-400">{usedPct.toFixed(1)}%</span>
<span className="text-white font-semibold">{ds.name}</span>
</div>
<div className="h-1.5 bg-gray-700 rounded overflow-hidden mb-2">
<div className={`h-full rounded ${barColor}`} style={{ width: `${Math.min(usedPct, 100)}%` }} />
</div>
<div className="flex items-center justify-between text-xs text-gray-500">
<span>Belegt: {formatBytes(ds.used)} / {formatBytes(ds.total)}</span>
<span>Frei: {formatBytes(ds.available)}</span>
</div>
{gc && (
<div className="mt-2 pt-2 border-t border-gray-700/50 text-xs text-gray-500 flex gap-4">
<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>Dauer: {formatDuration(gc.duration)}</span>}
</div>
{ds.backup_count > 0 && (
<span className="text-sm text-gray-400">{ds.backup_count} Backups</span>
)}
</div>
)
})}
</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>
</div>
{/* GC-Status */}
{gc && 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="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" />
</div>
</div>
)
})}
</div>
)}
{/* Tasks */}
{pbs.tasks?.length > 0 && (
<>
<h3 className="text-sm font-medium text-gray-300 mt-4">Letzte Tasks</h3>
<div className="space-y-2">
<h3 className="text-sm font-medium text-gray-400">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>
@ -1039,24 +1093,22 @@ function PBSTab({ pbs }) {
<th className="px-4 py-2">Typ</th>
<th className="px-4 py-2">Start</th>
<th className="px-4 py-2">Dauer</th>
<th className="px-4 py-2">Benutzer</th>
<th className="px-4 py-2 text-right">Status</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-700/50">
{pbs.tasks.map((t, i) => (
<tr key={i}>
<tr key={i} className="hover:bg-gray-700/20">
<td className="px-4 py-2 text-gray-300 font-mono">{t.type || '—'}</td>
<td className="px-4 py-2 text-gray-400">{formatTs(t.start_time)}</td>
<td className="px-4 py-2 text-gray-400">{formatDuration(t.duration)}</td>
<td className="px-4 py-2 text-gray-500">{t.user || '—'}</td>
<td className={`px-4 py-2 text-right font-medium ${taskStatusColor(t.status)}`}>{t.status || '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
</>
</div>
)}
</div>
)