Files
ProxMenux/AppImage/components/storage-display-card.tsx
DrOwlandClaude Opus 5.5 30034e7aa3 Map storage devices to their PCI storage controller
Each disk in /api/hardware now carries a controller_slot, resolved from
the disk's sysfs device path: the deepest PCI address in that path is the
controller (bridges sit above it), normalised to lspci's slot format so it
matches pci_devices[].slot directly. NVMe falls back to the controller's
own sysfs link when the namespace resolves to a subsystem with no PCI
parent; zvols and other virtual devices get None.

Hardware page:
- The disk dialog shows its storage controller (click through to the
  controller), and the PCI device dialog lists the disks attached to it.
- Storage controllers in the PCI Devices list get a badge with the
  number of attached disks.

Storage view:
- /api/storage returns controller_slot on each physical disk plus a
  controllers map (slot -> class, vendor, device, subsystem name,
  driver), built from the cached `lspci -vmm` output so the Storage view
  doesn't need the heavier /api/hardware enumeration.
- "Physical Disks & SMART Status" can group disks under their storage
  controller, with a header showing the controller name, slot, driver
  and disk count. Groups follow PCI slot order; disks with no resolvable
  controller are listed last. USB disks keep their own section.
- The disk details dialog shows the storage controller on the Overview
  tab.
- Grouping is opt-in via a new Storage Display card in Settings, stored
  per browser (localStorage) and synced across open views, following the
  existing nav-tab-order preference pattern. Off by default.

Also:
- Treat "IDE interface" as a storage controller, so SATA controllers left
  in BIOS IDE mode (e.g. Intel 7 Series, ata_piix) appear in pci_devices
  and disks behind them can be mapped.
- lspci prints "Device xxxx" when it has no subsystem name; that
  placeholder is ignored so the chip name is shown instead.
- Add the missing sdevice field to the PCIDevice type.

Tests cover the sysfs path -> controller slot rule against real paths
from two PVE 9 hosts (SAS HBA, AHCI SATA, SATA in BIOS IDE mode, USB
behind a hub, NVMe behind chipset bridges), the non-zero PCI domain case
(Intel VMD), the NVMe multipath fallback, and get_storage_controllers'
lspci parsing including the "Device xxxx" placeholder and driver lookup.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-29 11:47:00 +00:00

91 lines
3.4 KiB
TypeScript

"use client"
import { useState } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "./ui/card"
import { Switch } from "./ui/switch"
import { Check, HardDrive, Settings2 } from "lucide-react"
import { useT } from "../lib/i18n/provider"
import { useGroupDisksByController } from "../lib/storage-display"
export function StorageDisplayCard() {
const t = useT()
const [groupByController, setGroupByController] = useGroupDisksByController()
const [editMode, setEditMode] = useState(false)
const [pending, setPending] = useState<boolean | null>(null)
const [saved, setSaved] = useState(false)
const handleCancel = () => {
setPending(null)
setEditMode(false)
}
const handleSave = () => {
if (pending !== null && pending !== groupByController) {
setGroupByController(pending)
}
setPending(null)
setEditMode(false)
setSaved(true)
setTimeout(() => setSaved(false), 2000)
}
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<HardDrive className="h-5 w-5 text-blue-500" />
<CardTitle>{t("settings.storageDisplay.title")}</CardTitle>
</div>
<div className="flex items-center gap-2">
{saved && (
<span className="flex items-center gap-1 text-xs text-green-500">
<Check className="h-3.5 w-3.5" />
{t("status.saved")}
</span>
)}
{editMode ? (
<>
<button
className="h-7 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors text-muted-foreground"
onClick={handleCancel}
>
{t("actions.cancel")}
</button>
<button
className="h-7 px-3 text-xs rounded-md bg-blue-600 hover:bg-blue-700 text-white transition-colors disabled:opacity-50 flex items-center gap-1.5"
onClick={handleSave}
disabled={pending === null || pending === groupByController}
>
<Check className="h-3 w-3" />
{t("actions.save")}
</button>
</>
) : (
<button
className="h-7 px-3 text-xs rounded-md border border-border bg-background hover:bg-muted transition-colors flex items-center gap-1.5"
onClick={() => setEditMode(true)}
>
<Settings2 className="h-3 w-3" />
{t("actions.edit")}
</button>
)}
</div>
</div>
<CardDescription>{t("settings.storageDisplay.description")}</CardDescription>
</CardHeader>
<CardContent className={editMode ? "bg-accent" : undefined}>
<div className="text-foreground flex items-center justify-between gap-4">
<div className="flex items-center">{t("settings.storageDisplay.groupByController")}</div>
<Switch
checked={pending ?? groupByController}
disabled={!editMode}
onCheckedChange={(checked) => setPending(checked)}
className={`data-[state=checked]:bg-blue-600 data-[state=unchecked]:bg-input border border-border ${!editMode ? "opacity-60" : ""}`}
/>
</div>
</CardContent>
</Card>
)
}