feat: PBS — Disk-Auslastung via df, laufende Tasks, Kacheln kleiner ohne Debian-Version

This commit is contained in:
cynfo3000 2026-03-07 00:08:01 +01:00
parent 6487c2cf59
commit 3c83cf11de
2 changed files with 46 additions and 20 deletions

View File

@ -37,6 +37,7 @@ type PBSTask struct {
Duration int64 `json:"duration,omitempty"` Duration int64 `json:"duration,omitempty"`
User string `json:"user,omitempty"` User string `json:"user,omitempty"`
Datastore string `json:"datastore,omitempty"` Datastore string `json:"datastore,omitempty"`
Running bool `json:"running,omitempty"`
} }
type PBSGCStatus struct { type PBSGCStatus struct {
@ -90,10 +91,28 @@ func CollectPBS() *PBSInfo {
if avail, ok := d["avail"].(float64); ok { if avail, ok := d["avail"].(float64); ok {
ds.Available = int64(avail) ds.Available = int64(avail)
} }
// Backup-Anzahl: alle Timestamp-Verzeichnisse zaehlen // Disk-Auslastung via df
// Struktur: <path>/<type>/<id>/<timestamp> oder <path>/ns/<ns>/<type>/<id>/<timestamp>
// Timestamp-Format: 2026-02-18T18:24:36Z
if ds.Path != "" { if ds.Path != "" {
dfCmd := exec.Command("/bin/df", "-B1", "--output=size,used,avail", ds.Path)
if out, err := dfCmd.Output(); err == nil {
lines := strings.Split(strings.TrimSpace(string(out)), "\n")
if len(lines) >= 2 {
fields := strings.Fields(lines[1])
if len(fields) >= 3 {
if v, err := strconv.ParseInt(fields[0], 10, 64); err == nil {
ds.Total = v
}
if v, err := strconv.ParseInt(fields[1], 10, 64); err == nil {
ds.Used = v
}
if v, err := strconv.ParseInt(fields[2], 10, 64); err == nil {
ds.Available = v
}
}
}
}
// Backup-Anzahl: alle Timestamp-Verzeichnisse zaehlen
countCmd := exec.Command("/bin/sh", "-c", countCmd := exec.Command("/bin/sh", "-c",
`find `+ds.Path+` -type d 2>/dev/null | grep -cE '/[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$'`) `find `+ds.Path+` -type d 2>/dev/null | grep -cE '/[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}Z$'`)
if out, err := countCmd.Output(); err == nil { if out, err := countCmd.Output(); err == nil {
@ -131,11 +150,14 @@ func CollectPBS() *PBSInfo {
if start, ok := t["starttime"].(float64); ok { if start, ok := t["starttime"].(float64); ok {
task.StartTime = int64(start) task.StartTime = int64(start)
} }
if end, ok := t["endtime"].(float64); ok { if end, ok := t["endtime"].(float64); ok && end > 0 {
task.EndTime = int64(end) task.EndTime = int64(end)
if task.StartTime > 0 { if task.StartTime > 0 {
task.Duration = task.EndTime - task.StartTime task.Duration = task.EndTime - task.StartTime
} }
} else {
task.Running = true
task.Status = "running"
} }
if user, ok := t["user"].(string); ok { if user, ok := t["user"].(string); ok {
task.User = user task.User = user

View File

@ -1003,12 +1003,12 @@ function PBSTab({ pbs, agentId, sys }) {
} }
const totalBackups = pbs.datastores?.reduce((s, d) => s + (d.backup_count || 0), 0) || 0 const totalBackups = pbs.datastores?.reduce((s, d) => s + (d.backup_count || 0), 0) || 0
const kernel = sys?.opnsense_version || '—' const runningTasks = pbs.tasks?.filter(t => t.running)?.length || 0
const StatCard = ({ icon, value, label, iconColor }) => ( const StatCard = ({ icon, value, label, iconColor, highlight }) => (
<div className="flex-1 bg-[#1a2235] rounded-xl border border-gray-700/40 p-4 flex flex-col items-center gap-2"> <div className={`flex-1 bg-[#1a2235] rounded-xl border p-3 flex flex-col items-center gap-1.5 ${highlight ? 'border-blue-500/50' : 'border-gray-700/40'}`}>
<div className={`text-2xl ${iconColor}`}>{icon}</div> <div className={`text-xl ${iconColor}`}>{icon}</div>
<div className="text-2xl font-bold text-white">{value}</div> <div className="text-xl font-bold text-white">{value}</div>
<div className="text-xs text-gray-400">{label}</div> <div className="text-xs text-gray-400">{label}</div>
</div> </div>
) )
@ -1022,30 +1022,31 @@ function PBSTab({ pbs, agentId, sys }) {
</div> </div>
{/* Stat-Karten */} {/* Stat-Karten */}
<div className="flex gap-3"> <div className="flex gap-2">
<StatCard <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>} icon={<svg className="w-5 h-5" 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} value={pbs.datastores?.length || 0}
label="Datastores" label="Datastores"
iconColor="text-orange-400" iconColor="text-orange-400"
/> />
<StatCard <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>} icon={<svg className="w-5 h-5" 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} value={totalBackups}
label="Backups" label="Backups"
iconColor="text-blue-400" iconColor="text-blue-400"
/> />
<StatCard <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>} icon={<svg className="w-5 h-5" 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} value={pbs.sync_jobs ?? 0}
label="Sync Jobs" label="Sync Jobs"
iconColor="text-green-400" iconColor="text-green-400"
/> />
<StatCard <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>} icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10" strokeWidth="2"/><polyline points="12 6 12 12 16 14" strokeWidth="2"/></svg>}
value={kernel !== '—' ? kernel.split('-')[0] : '—'} value={runningTasks > 0 ? runningTasks : '—'}
label="Kernel" label="Laufend"
iconColor="text-orange-400" iconColor={runningTasks > 0 ? "text-blue-400" : "text-gray-600"}
highlight={runningTasks > 0}
/> />
</div> </div>
@ -1125,10 +1126,13 @@ function PBSTab({ pbs, agentId, sys }) {
</thead> </thead>
<tbody className="divide-y divide-gray-700/50"> <tbody className="divide-y divide-gray-700/50">
{pbs.tasks.map((t, i) => ( {pbs.tasks.map((t, i) => (
<tr key={i} className="hover:bg-gray-700/20"> <tr key={i} className={`hover:bg-gray-700/20 ${t.running ? 'bg-blue-900/10' : ''}`}>
<td className="px-4 py-2 text-gray-300 font-mono">{t.type || '—'}</td> <td className="px-4 py-2 font-mono">
<span className={t.running ? 'text-blue-300 font-semibold' : 'text-gray-300'}>{t.type || '—'}</span>
{t.running && <span className="ml-2 text-[10px] font-bold px-1.5 py-0.5 rounded bg-blue-500/20 text-blue-400 border border-blue-500/30">RUNNING</span>}
</td>
<td className="px-4 py-2 text-gray-400">{formatTs(t.start_time)}</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-400">{t.running ? <span className="text-blue-400 animate-pulse">läuft...</span> : formatDuration(t.duration)}</td>
<td className={`px-4 py-2 text-right font-medium ${taskStatusColor(t.status)}`}>{t.status || '—'}</td> <td className={`px-4 py-2 text-right font-medium ${taskStatusColor(t.status)}`}>{t.status || '—'}</td>
</tr> </tr>
))} ))}