Feature: ZFS Pool-Aktionen (Scrub, Resilver, Clear Errors) + Product-Spalte fix

- Backend: neue Endpoints POST /api/pools/{name}/clear und /resilver
- Frontend: Pool ⋮-Menü mit Scrub, Resilver, Clear Errors
- Product-Spalte im Status-Tab bricht jetzt korrekt um statt abgeschnitten zu werden

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 00:45:35 +02:00
parent 1dc55b189b
commit 6f6e8555af
3 changed files with 89 additions and 2 deletions
+47 -2
View File
@@ -79,7 +79,7 @@ function VdevTree({ vdevs, depth = 0 }: { vdevs: any[]; depth?: number }) {
<td className="px-4 py-2 text-xs text-center">{v.write ?? 0}</td>
<td className="px-4 py-2 text-xs text-center">{v.cksum ?? 0}</td>
<td className="px-4 py-2 text-xs text-muted-foreground">{v.message || ""}</td>
<td className="px-4 py-2 text-xs text-muted-foreground">{v.product || ""}</td>
<td className="px-4 py-2 text-xs text-muted-foreground break-all">{v.product || ""}</td>
<td className="px-4 py-2 text-xs">
<button className="p-1 rounded hover:bg-muted">
<MoreVertical className="w-3 h-3" />
@@ -97,6 +97,9 @@ export default function ZfsPage() {
const [lastRefresh, setLastRefresh] = useState<Date | null>(null)
const [poolStates, setPoolStates] = useState<Map<string, PoolRowState>>(new Map())
// Pool context menu
const [poolMenu, setPoolMenu] = useState<{ pool: Pool; x: number; y: number } | null>(null)
// Dialogs
const [showCreateFilesystem, setShowCreateFilesystem] = useState(false)
const [createFsPool, setCreateFsPool] = useState("")
@@ -250,6 +253,20 @@ export default function ZfsPage() {
setCloneTarget(null)
}
const handlePoolAction = async (action: string, pool: Pool) => {
setPoolMenu(null)
if (action === "scrub") {
await api.startScrub(pool.name)
refreshPoolStatus(pool.name)
} else if (action === "clear") {
await api.clearPoolErrors(pool.name)
refreshPoolStatus(pool.name)
} else if (action === "resilver") {
await api.resilverPool(pool.name)
refreshPoolStatus(pool.name)
}
}
const handleCreateFilesystem = async () => {
if (!newFsName.trim()) return
const fullName = createFsPool ? `${createFsPool}/${newFsName.trim()}` : newFsName.trim()
@@ -359,7 +376,10 @@ export default function ZfsPage() {
<UsageBar alloc={pool.alloc} size={pool.size} />
</td>
<td className="px-4 py-3 text-right" onClick={(e) => e.stopPropagation()}>
<button className="p-1 rounded hover:bg-muted">
<button
className="p-1 rounded hover:bg-muted"
onClick={(e) => setPoolMenu({ pool, x: e.clientX, y: e.clientY })}
>
<MoreVertical className="w-4 h-4" />
</button>
</td>
@@ -696,6 +716,31 @@ export default function ZfsPage() {
</div>
</Dialog>
{/* Pool Context Menu */}
{poolMenu && (
<>
<div className="fixed inset-0 z-40" onClick={() => setPoolMenu(null)} />
<div
className="fixed z-50 bg-background border border-border rounded-md shadow-lg py-1 min-w-[220px]"
style={{ top: poolMenu.y, left: poolMenu.x }}
>
{[
{ action: "scrub", label: "Scrub Storage Pool" },
{ action: "resilver", label: "Resilver Storage Pool" },
{ action: "clear", label: "Clear Storage Pool Errors" },
].map(({ action, label }) => (
<button
key={action}
className="w-full text-left px-4 py-2 text-sm hover:bg-muted"
onClick={() => handlePoolAction(action, poolMenu.pool)}
>
{label}
</button>
))}
</div>
</>
)}
{/* Snapshot Context Menu */}
{snapContextMenu && (
<>