feat: PBS Datastores — Backup-Anzahl, neues Layout mit GC/Verify/Prune Buttons
This commit is contained in:
parent
a036849565
commit
67651a5aeb
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user