"use client"; import { useState } from "react"; import { toast } from "sonner"; import { cn } from "@/lib/utils"; import { PANEL_INSET } from "@/lib/ui/surface-tokens"; import { useDisks, useDiskFormat, useDiskMount, useDiskUnmount, useDiskCreatePartition, useDiskDeletePartition, useDiskWipe, } from "@/modules/system/hooks/useDisks"; import type { DiskDevice, DiskFilesystem, DiskPartition } from "@/lib/shared/contracts/disks"; import { DISK_FILESYSTEMS } from "@/lib/shared/contracts/disks"; import { RefreshCw as ArrowSyncRegular, HardDrive as HardDriveRegular, AlertTriangle as WarningRegular, X as DismissRegular, Check as CheckmarkRegular, Download as ArrowDownloadRegular, Upload as ArrowUploadRegular, Trash2 as DeleteRegular, Plus as AddRegular, } from "@/components/icons/platform-icons"; // ─── helpers ────────────────────────────────────────────────────────────────── function formatBytes(bytes: number | null): string { if (bytes === null) return "—"; if (bytes >= 1e12) return `${(bytes / 1e12).toFixed(1)} TB`; if (bytes >= 1e9) return `${(bytes / 1e9).toFixed(1)} GB`; if (bytes >= 1e6) return `${(bytes / 1e6).toFixed(0)} MB`; return `${(bytes / 1e3).toFixed(0)} KB`; } const FS_COLOR: Record = { ext4: "bg-primary/70", ext3: "bg-primary/50", btrfs: "bg-violet-500/70", xfs: "bg-emerald-500/70", ntfs: "bg-orange-400/70", vfat: "bg-sky-400/70", exfat: "bg-sky-500/60", swap: "bg-status-red/60", }; function fsColor(fstype: string | null): string { return fstype ? (FS_COLOR[fstype.toLowerCase()] ?? "bg-muted-foreground/30") : "bg-muted-foreground/20"; } const MEDIA_BADGE: Record = { nvme: { label: "NVMe", color: "text-violet-400" }, ssd: { label: "SSD", color: "text-sky-400" }, hdd: { label: "HDD", color: "text-amber-400" }, removable: { label: "USB", color: "text-emerald-400" }, unknown: { label: "Disk", color: "text-muted-foreground/60" }, }; // ─── sub-components ─────────────────────────────────────────────────────────── function DiskCard({ disk, selected, onClick }: { disk: DiskDevice; selected: boolean; onClick: () => void }) { const badge = MEDIA_BADGE[disk.mediaType] ?? MEDIA_BADGE.unknown!; return ( ); } function PartitionBar({ partitions, totalBytes }: { partitions: DiskPartition[]; totalBytes: number | null }) { if (!totalBytes || totalBytes === 0) return null; const sized = partitions.filter((p) => p.sizeBytes !== null && p.sizeBytes > 0); const usedBytes = sized.reduce((s, p) => s + (p.sizeBytes ?? 0), 0); const freeBytes = Math.max(0, totalBytes - usedBytes); const freePct = (freeBytes / totalBytes) * 100; return (
{sized.map((p) => { const pct = ((p.sizeBytes ?? 0) / totalBytes) * 100; return (
); })} {freePct > 0.5 && (
)}
); } // ─── confirm banner ─────────────────────────────────────────────────────────── type ConfirmState = | { type: "format"; device: string; filesystem: DiskFilesystem; label: string } | { type: "delete"; device: string } | { type: "wipe"; disk: string }; function ConfirmBanner({ state, onConfirm, onCancel, isPending, }: { state: ConfirmState; onConfirm: () => void; onCancel: () => void; isPending: boolean; }) { const messages: Record = { format: `Format ${state.type === "format" ? state.device : ""} as ${state.type === "format" ? state.filesystem : ""}? All data will be lost.`, delete: `Delete partition ${state.type === "delete" ? state.device : ""}? This cannot be undone.`, wipe: `Wipe all data and partitions from ${state.type === "wipe" ? state.disk : ""}? This is irreversible.`, }; return (

{messages[state.type]}

); } // ─── mount form ─────────────────────────────────────────────────────────────── function MountForm({ device, onMount, onCancel, isPending, }: { device: string; onMount: (mountPoint: string, addToFstab: boolean) => void; onCancel: () => void; isPending: boolean; }) { const safeName = device.replace(/[^a-zA-Z0-9]/g, "_").replace(/^_+/, ""); const [mountPoint, setMountPoint] = useState(`/mnt/${safeName}`); const [addToFstab, setAddToFstab] = useState(false); return (

Mount {device}

setMountPoint(e.target.value)} className="rounded-lg border border-glass-border bg-background/55 px-3 py-1.5 text-[12px] text-foreground outline-none focus:border-primary/40" placeholder="/mnt/data" />
); } // ─── format form ────────────────────────────────────────────────────────────── function FormatForm({ device, onFormat, onCancel, }: { device: string; onFormat: (filesystem: DiskFilesystem, label: string) => void; onCancel: () => void; }) { const [filesystem, setFilesystem] = useState("ext4"); const [label, setLabel] = useState(""); return (

Format {device}

setLabel(e.target.value)} placeholder="data" className="rounded-lg border border-glass-border bg-background/55 px-3 py-1.5 text-[12px] text-foreground outline-none focus:border-primary/40" />
); } // ─── create partition form ──────────────────────────────────────────────────── function CreatePartitionForm({ disk, onCreate, onCancel, isPending, }: { disk: string; onCreate: (start: string, end: string) => void; onCancel: () => void; isPending: boolean; }) { const [start, setStart] = useState("1MiB"); const [end, setEnd] = useState("100%"); return (

New partition on {disk}

{[ { label: "Start", value: start, setter: setStart, placeholder: "1MiB or 0%" }, { label: "End", value: end, setter: setEnd, placeholder: "100% or 50GiB" }, ].map(({ label, value, setter, placeholder }) => (
setter(e.target.value)} placeholder={placeholder} className="rounded-lg border border-glass-border bg-background/55 px-3 py-1.5 text-[12px] font-mono text-foreground outline-none focus:border-primary/40" />
))}

Use % for proportional (e.g. 0%, 100%) or size units (1MiB, 50GiB).

); } // ─── partition row ──────────────────────────────────────────────────────────── function PartitionRow({ partition, onFormat, onMount, onUnmount, onDelete, }: { partition: DiskPartition; onFormat: () => void; onMount: () => void; onUnmount: () => void; onDelete: () => void; }) { const isMounted = Boolean(partition.mountpoint); return (
{partition.device}
{partition.fstype ?? —} {formatBytes(partition.sizeBytes)} {partition.mountpoint ?? "—"}
{isMounted ? ( ) : ( )}
); } // ─── disk detail panel ──────────────────────────────────────────────────────── type ActiveForm = | { type: "format"; device: string } | { type: "mount"; device: string } | { type: "create-partition" } | null; function DiskDetail({ disk }: { disk: DiskDevice }) { const [activeForm, setActiveForm] = useState(null); const [confirmState, setConfirmState] = useState(null); const [pendingFormat, setPendingFormat] = useState<{ filesystem: DiskFilesystem; label: string } | null>(null); const formatMutation = useDiskFormat(); const mountMutation = useDiskMount(); const unmountMutation = useDiskUnmount(); const createPartitionMutation = useDiskCreatePartition(); const deletePartitionMutation = useDiskDeletePartition(); const wipeMutation = useDiskWipe(); const anyPending = formatMutation.isPending || mountMutation.isPending || unmountMutation.isPending || createPartitionMutation.isPending || deletePartitionMutation.isPending || wipeMutation.isPending; async function handleConfirm() { if (!confirmState) return; try { if (confirmState.type === "format") { await formatMutation.mutateAsync({ device: confirmState.device, filesystem: confirmState.filesystem, label: confirmState.label || undefined, }); toast.success(`Formatted ${confirmState.device} as ${confirmState.filesystem}`); } else if (confirmState.type === "delete") { await deletePartitionMutation.mutateAsync({ device: confirmState.device }); toast.success(`Deleted partition ${confirmState.device}`); } else if (confirmState.type === "wipe") { await wipeMutation.mutateAsync({ disk: confirmState.disk }); toast.success(`Wiped disk ${confirmState.disk}`); } } catch (e) { toast.error(e instanceof Error ? e.message : "Action failed"); } finally { setConfirmState(null); setPendingFormat(null); setActiveForm(null); } } function handleFormatSubmit(filesystem: DiskFilesystem, label: string) { if (activeForm?.type !== "format") return; setPendingFormat({ filesystem, label }); setConfirmState({ type: "format", device: activeForm.device, filesystem, label }); setActiveForm(null); } async function handleMount(mountPoint: string, addToFstab: boolean) { if (activeForm?.type !== "mount") return; try { await mountMutation.mutateAsync({ device: activeForm.device, mountPoint, addToFstab }); toast.success(`Mounted ${activeForm.device} at ${mountPoint}`); setActiveForm(null); } catch (e) { toast.error(e instanceof Error ? e.message : "Mount failed"); } } async function handleUnmount(device: string) { try { await unmountMutation.mutateAsync({ device }); toast.success(`Unmounted ${device}`); } catch (e) { toast.error(e instanceof Error ? e.message : "Unmount failed"); } } async function handleCreatePartition(start: string, end: string) { try { await createPartitionMutation.mutateAsync({ disk: disk.device, start, end }); toast.success("Partition created"); setActiveForm(null); } catch (e) { toast.error(e instanceof Error ? e.message : "Create failed"); } } return (
{/* Disk header */}

{disk.model ?? disk.device}

{disk.device} · {formatBytes(disk.sizeBytes)} · {MEDIA_BADGE[disk.mediaType]?.label ?? "Disk"}

{disk.serial && ( {disk.serial} )}
{/* Partition bar */} {disk.partitions.length > 0 && ( )} {/* Partition table */}
Partition Filesystem Size Mount Point
{disk.partitions.length === 0 ? (

No partitions found

) : (
{disk.partitions.map((partition) => ( setActiveForm({ type: "format", device: partition.device })} onMount={() => setActiveForm({ type: "mount", device: partition.device })} onUnmount={() => handleUnmount(partition.device)} onDelete={() => setConfirmState({ type: "delete", device: partition.device })} /> ))}
)}
{/* Inline forms */} {activeForm?.type === "format" && ( setActiveForm(null)} /> )} {activeForm?.type === "mount" && ( setActiveForm(null)} isPending={mountMutation.isPending} /> )} {activeForm?.type === "create-partition" && ( setActiveForm(null)} isPending={createPartitionMutation.isPending} /> )} {/* Confirm banner */} {confirmState && ( { setConfirmState(null); setPendingFormat(null); }} isPending={anyPending} /> )} {/* Disk-level actions */}
); } // ─── DiskManager (root) ─────────────────────────────────────────────────────── export function DiskManager() { const { data, isLoading, refetch, isRefetching } = useDisks(); const [selectedDisk, setSelectedDisk] = useState(null); const disks = data?.disks ?? []; const active = disks.find((d) => d.name === selectedDisk) ?? disks[0] ?? null; return (
{/* Header */}
{isLoading ? "Loading…" : `${disks.length} disk${disks.length !== 1 ? "s" : ""} detected`}
{data?.readOnly ? (

Homeio runs in Docker, so disks are shown read-only: formatting, partitioning, mounting and wiping need the script install on the host.

) : null} {/* Body */}
{/* Disk list sidebar */}
{isLoading ? (

Detecting disks…

) : disks.length === 0 ? (

{data?.unavailableReason ?? "No disks found"}

) : ( disks.map((disk) => ( setSelectedDisk(disk.name)} /> )) )}
{/* Detail panel */} {active ? ( // disabled on a fieldset disables every button inside it.
) : (

Select a disk

)}
); }