# BUGFIX: Storage Scan — System Disk Detection & FSType in Container **Affects:** v0.11.0, `internal/storage/scan_linux.go` **Root cause:** Controller runs in a Docker container. Even with `--privileged`, `lsblk` reports mount points from the container's mount namespace (not host), and often can't probe filesystem types due to missing udev/blkid cache. ## Bug 1: System disk (sda) shows as available ### Current broken logic ```go if part.MountPoint == "/" || part.MountPoint == "/boot" || part.MountPoint == "/boot/efi" { isSystem = true } ``` Inside the container, sda2 (host's `/`) shows mounted at `/opt/docker/felhom-controller/data` (bind mount), not `/`. So `isSystem` stays false → sda appears in AvailableDisks. ### Fix: Parse host's fstab + blkid to detect system disk The host's fstab is mounted at `/host-fstab` inside the container. Parse it to find which devices/UUIDs are used for `/`, `/boot`, `/boot/efi`, and `swap`. Then resolve UUIDs to device paths via `blkid`, and mark their parent disks as system disks. ```go // getSystemDiskNames returns the set of parent disk names (e.g., "sda") // that contain system partitions (/, /boot, /boot/efi, swap). func getSystemDiskNames() map[string]bool { systemDisks := map[string]bool{} // Step 1: Parse /host-fstab for system mount points fstabPath := "/host-fstab" if _, err := os.Stat(fstabPath); err != nil { // Fallback: try /etc/fstab (if not containerized or different mount) fstabPath = "/etc/fstab" } data, err := os.ReadFile(fstabPath) if err != nil { return systemDisks // Can't read fstab, return empty (safe default: nothing excluded) } // System mount points we care about systemMounts := map[string]bool{"/": true, "/boot": true, "/boot/efi": true} var systemUUIDs []string var systemDevices []string for _, line := range strings.Split(string(data), "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } fields := strings.Fields(line) if len(fields) < 3 { continue } source := fields[0] mountPoint := fields[1] fsType := fields[2] isSystemEntry := systemMounts[mountPoint] || fsType == "swap" if !isSystemEntry { continue } if strings.HasPrefix(source, "UUID=") { systemUUIDs = append(systemUUIDs, strings.TrimPrefix(source, "UUID=")) } else if strings.HasPrefix(source, "/dev/") { systemDevices = append(systemDevices, source) } } // Step 2: Resolve UUIDs to device paths via blkid for _, uuid := range systemUUIDs { out, err := exec.Command("blkid", "-U", uuid).Output() if err == nil { devPath := strings.TrimSpace(string(out)) // e.g., "/dev/sda2" systemDevices = append(systemDevices, devPath) } } // Step 3: Extract parent disk names from device paths for _, devPath := range systemDevices { diskName := partitionToParentDisk(devPath) if diskName != "" { systemDisks[diskName] = true } } return systemDisks } // partitionToParentDisk extracts parent disk name from a partition device path. // "/dev/sda2" → "sda", "/dev/nvme0n1p2" → "nvme0n1" func partitionToParentDisk(devPath string) string { name := filepath.Base(devPath) // "sda2" // NVMe: nvme0n1p2 → nvme0n1 if strings.Contains(name, "nvme") { if idx := strings.LastIndex(name, "p"); idx > 0 { candidate := name[:idx] // Verify it's actually a partition number after 'p' if _, err := strconv.Atoi(name[idx+1:]); err == nil { return candidate } } return name } // Standard: sda2 → sda, sdb1 → sdb return strings.TrimRight(name, "0123456789") } ``` Then in `ScanDisks()`, replace the mount-point-based detection: ```go func ScanDisks() (*ScanResult, error) { // ... lsblk parsing as before ... // Get system disk names from host fstab systemDiskNames := getSystemDiskNames() for _, dev := range parsed.BlockDevices { if dev.Type != "disk" { continue } // ... build BlockDevice as before ... // Check if this is a system disk (from fstab analysis) isSystem := systemDiskNames[dev.Name] // Also check if any partition is currently mounted (fallback safety) anyMounted := false for _, child := range dev.Children { // ... as before ... if part.MountPoint != "" { anyMounted = true } } bd.Mounted = anyMounted || isSystem if isSystem || anyMounted { result.SystemDisks = append(result.SystemDisks, bd) } else { result.AvailableDisks = append(result.AvailableDisks, bd) } } return result, nil } ``` ## Bug 2: "nincs fájlrendszer" for all partitions ### Current broken logic `lsblk` inside a container often returns `null` for `fstype` because it relies on udev/blkid cache that's incomplete in the container's environment. ### Fix: Enrich with blkid After lsblk parsing, run `blkid` to get filesystem types for all partitions. `blkid` directly probes the device (works in privileged containers): ```go // enrichWithBlkid fills in missing FSType, UUID, and Label from blkid. func enrichWithBlkid(disks []BlockDevice) { // Run blkid once for all devices out, err := exec.Command("blkid", "-o", "export").Output() if err != nil { return // Best-effort; lsblk data still usable } // Parse blkid output — blocks separated by blank lines: // DEVNAME=/dev/sda1 // UUID=XXXX-YYYY // TYPE=vfat // ... blkidMap := parseBlkidExport(out) for i := range disks { for j := range disks[i].Partitions { p := &disks[i].Partitions[j] if info, ok := blkidMap[p.Path]; ok { if p.FSType == "" { p.FSType = info.FSType } if p.UUID == "" { p.UUID = info.UUID } if p.Label == "" { p.Label = info.Label } } } } } type blkidInfo struct { FSType string UUID string Label string } func parseBlkidExport(data []byte) map[string]blkidInfo { result := map[string]blkidInfo{} blocks := strings.Split(string(data), "\n\n") for _, block := range blocks { var devName string info := blkidInfo{} for _, line := range strings.Split(strings.TrimSpace(block), "\n") { parts := strings.SplitN(line, "=", 2) if len(parts) != 2 { continue } key, val := parts[0], parts[1] switch key { case "DEVNAME": devName = val case "TYPE": info.FSType = val case "UUID": info.UUID = val case "LABEL": info.Label = val } } if devName != "" { result[devName] = info } } return result } ``` Call `enrichWithBlkid()` at the end of `ScanDisks()` on both `AvailableDisks` and `SystemDisks`. ## UI impact After these fixes: - sda will appear in `SystemDisks` (shown grayed out with "Rendszermeghajtó" label, or hidden entirely) - sdb will be the only entry in `AvailableDisks` - sda partitions will show: sda1 (vfat, /boot/efi), sda2 (ext4, /), sda3 (swap) - sdb1 will correctly show "(nincs fájlrendszer)" since it genuinely has none ## Template update If SystemDisks are currently shown alongside AvailableDisks (both selectable), the template should either: - **Option A:** Hide system disks entirely — simpler, less confusion - **Option B:** Show them grayed out with a "Rendszermeghajtó — nem választható" badge Recommended: **Option A** — only show AvailableDisks. The user doesn't need to see sda at all. ## Files modified - `controller/internal/storage/scan_linux.go` — `getSystemDiskNames()`, `partitionToParentDisk()`, `enrichWithBlkid()`, `parseBlkidExport()`, updated `ScanDisks()` ## Testing 1. After fix: scan page shows only sdb (931.5 GB, HD710 PRO, sdb1 no filesystem) 2. sda is no longer listed 3. If you temporarily disconnect the USB HDD: scan shows "Nem található inicializálható meghajtó"