feat: format empty partitions on system disk (v0.32.6)

Detect and offer to format empty (no filesystem) partitions on the system
disk. Adds IsSystemPartition() for granular per-partition safety checks
instead of blocking the entire system disk. Init wizard shows formatable
partitions with appropriate warnings. Add felhotest demo node to docs.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-27 16:54:16 +01:00
parent 2c0064ac87
commit b4bda38fa1
11 changed files with 270 additions and 39 deletions
+12
View File
@@ -1,5 +1,17 @@
## Changelog ## Changelog
### v0.32.6 — Format empty partitions on system disk (2026-02-27)
#### Added
- **storage/scan.go**: New `FormatablePartition` struct and `FormatablePartitions` field on `ScanResult` — detects empty (no filesystem), unmounted, non-system partitions on system disks
- **storage/scan_linux.go**: New `getSystemPartitionPaths()` resolves actual system partition device paths from fstab (more granular than `getSystemDiskNames()` which returns parent disk names). `ScanDisks()` now populates `FormatablePartitions` after enrichment
- **storage/safety_linux.go**: New `IsSystemPartition()` — checks if a specific partition is a system partition (/, /boot, /boot/efi, swap) or is currently mounted; more granular than `IsSystemDisk()` which blocks the entire disk
- **web/storage_handlers.go**: Scan API response now includes `formatable_partitions` array
- **web/templates/storage_init.html**: Init wizard shows formatable system-disk partitions as a separate selectable section with info banner, conditional warning text, and hidden partitioning progress step
#### Changed
- **storage/format_linux.go**: `FormatAndMount()` now uses `IsSystemPartition()` for partition-only operations (`CreatePartition=false`) instead of `IsSystemDisk()` — allows formatting empty data partitions on the system disk while still blocking system partitions
### v0.32.5 — USB badge fix + graceful Tier2 backup on disconnected/inactive/removed destinations (2026-02-27) ### v0.32.5 — USB badge fix + graceful Tier2 backup on disconnected/inactive/removed destinations (2026-02-27)
#### Fixed #### Fixed
+13 -3
View File
@@ -36,6 +36,7 @@ Claude in Chrome extension is available — can be used to test web UI on demo-f
| **Local (this machine)** | Windows 11 | — | Development, Claude Code runs here. Repos in `E:\git\` | | **Local (this machine)** | Windows 11 | — | Development, Claude Code runs here. Repos in `E:\git\` |
| **Build server (k3s, infra)** | Debian 13 | 192.168.0.180 | Build + push container images, k3s cluster | | **Build server (k3s, infra)** | Debian 13 | 192.168.0.180 | Build + push container images, k3s cluster |
| **Demo node** | Debian 13 | 192.168.0.162 | Test deployment (demo-felhom.eu) | | **Demo node** | Debian 13 | 192.168.0.162 | Test deployment (demo-felhom.eu) |
| **Demo node 2** | Debian 13 | router.abonet.hu:33022 | Remote test deployment |
## Workspace layout ## Workspace layout
@@ -101,12 +102,14 @@ substitute the full path manually.
|------|----|----|------|------| |------|----|----|------|------|
| Build server | Debian 13 | 192.168.0.180 | kisfenyo | Build + push container images | | Build server | Debian 13 | 192.168.0.180 | kisfenyo | Build + push container images |
| Demo node | Debian 13 | 192.168.0.162 | kisfenyo | Test deployment (demo-felhom.eu) | | Demo node | Debian 13 | 192.168.0.162 | kisfenyo | Test deployment (demo-felhom.eu) |
| Demo node 2 | Debian 13 | router.abonet.hu (SSH port 33022) | kisfenyo | Remote test deployment |
## Test environments ## Test environments
| Node | OS | Hardware | Domain | IP | Notes | | Node | OS | Hardware | Domain | IP | Notes |
|------|-----|----------|--------|----|-------| |------|-----|----------|--------|----|-------|
| demo-felhom | Debian 13 | Acemagic N100, 16G RAM, 512G SSD + 1TB HDD | demo-felhom.eu | 192.168.0.162 | Primary test node, Cloudflare Tunnel | | demo-felhom | Debian 13 | Acemagic N100, 16G RAM, 512G SSD + 1TB HDD | demo-felhom.eu | 192.168.0.162 | Primary test node, Cloudflare Tunnel |
| felhotest | Debian 13 | Remote VPS, 200G + 100G disk | — | router.abonet.hu:33022 | Remote test node |
| pi-customer-1 | Debian 13 | Raspberry Pi 3B+, 1G RAM, 32G SD | pi-customer-1.local | 192.168.0.161 | Secondary test, not yet active | | pi-customer-1 | Debian 13 | Raspberry Pi 3B+, 1G RAM, 32G SD | pi-customer-1.local | 192.168.0.161 | Secondary test, not yet active |
- Pi-hole DNS on local network forwards `*.demo-felhom.eu` → 192.168.0.162 - Pi-hole DNS on local network forwards `*.demo-felhom.eu` → 192.168.0.162
@@ -152,21 +155,27 @@ The build script:
- Pushes to `gitea.dooplex.hu/admin/felhom-controller:<VERSION>` - Pushes to `gitea.dooplex.hu/admin/felhom-controller:<VERSION>`
- Expects the version as first argument (e.g., `0.2.11`) - Expects the version as first argument (e.g., `0.2.11`)
### Step 3: Deploy on the demo node ### Step 3: Deploy on demo nodes
```bash ```bash
# Demo node 1 (local)
$SSH kisfenyo@192.168.0.162 "cd /opt/docker/felhom-controller && sudo docker pull gitea.dooplex.hu/admin/felhom-controller:<NEW_VERSION> && sudo sed -i 's|image: gitea.dooplex.hu/admin/felhom-controller:.*|image: gitea.dooplex.hu/admin/felhom-controller:<NEW_VERSION>|' docker-compose.yml && sudo docker compose up -d" $SSH kisfenyo@192.168.0.162 "cd /opt/docker/felhom-controller && sudo docker pull gitea.dooplex.hu/admin/felhom-controller:<NEW_VERSION> && sudo sed -i 's|image: gitea.dooplex.hu/admin/felhom-controller:.*|image: gitea.dooplex.hu/admin/felhom-controller:<NEW_VERSION>|' docker-compose.yml && sudo docker compose up -d"
# Demo node 2 (remote)
$SSH -p 33022 kisfenyo@router.abonet.hu "cd /opt/docker/felhom-controller && sudo docker pull gitea.dooplex.hu/admin/felhom-controller:<NEW_VERSION> && sudo sed -i 's|image: gitea.dooplex.hu/admin/felhom-controller:.*|image: gitea.dooplex.hu/admin/felhom-controller:<NEW_VERSION>|' docker-compose.yml && sudo docker compose up -d"
``` ```
### Step 4: Verify the deployment ### Step 4: Verify the deployment
```bash ```bash
$SSH kisfenyo@192.168.0.162 "docker ps --filter name=felhom-controller --format '{{.Image}} {{.Status}}'" $SSH kisfenyo@192.168.0.162 "docker ps --filter name=felhom-controller --format '{{.Image}} {{.Status}}'"
$SSH -p 33022 kisfenyo@router.abonet.hu "docker ps --filter name=felhom-controller --format '{{.Image}} {{.Status}}'"
``` ```
Should show the new version and "Up" status. Also check logs for startup errors: Should show the new version and "Up" status. Also check logs for startup errors:
```bash ```bash
$SSH kisfenyo@192.168.0.162 "docker logs felhom-controller --tail 20" $SSH kisfenyo@192.168.0.162 "docker logs felhom-controller --tail 20"
$SSH -p 33022 kisfenyo@router.abonet.hu "docker logs felhom-controller --tail 20"
``` ```
### Build workflow summary ### Build workflow summary
@@ -176,8 +185,9 @@ $SSH kisfenyo@192.168.0.162 "docker logs felhom-controller --tail 20"
| 0. Set SSH var | `SSH=/c/Windows/System32/OpenSSH/ssh.exe` | Local (once per session) | | 0. Set SSH var | `SSH=/c/Windows/System32/OpenSSH/ssh.exe` | Local (once per session) |
| 1. Commit + push | `git add -A && git commit -m "..." && git push` | Local (this repo) | | 1. Commit + push | `git add -A && git commit -m "..." && git push` | Local (this repo) |
| 2. Build + push image | `$SSH kisfenyo@192.168.0.180 "cd ~/build/felhom-controller... ./build.sh <VER> --push"` | Build server | | 2. Build + push image | `$SSH kisfenyo@192.168.0.180 "cd ~/build/felhom-controller... ./build.sh <VER> --push"` | Build server |
| 3. Deploy | `$SSH kisfenyo@192.168.0.162 "... docker compose up -d"` | Demo node | | 3. Deploy (node 1) | `$SSH kisfenyo@192.168.0.162 "... docker compose up -d"` | Demo node |
| 4. Verify | `$SSH kisfenyo@192.168.0.162 "docker ps ..."` | Demo node | | 3b. Deploy (node 2) | `$SSH -p 33022 kisfenyo@router.abonet.hu "... docker compose up -d"` | Demo node 2 |
| 4. Verify | `$SSH kisfenyo@192.168.0.162 "docker ps ..."` + same for router.abonet.hu | Both nodes |
### Build & deploy workflow — Hub (felhom-hub) ### Build & deploy workflow — Hub (felhom-hub)
+1 -1
View File
@@ -40,7 +40,7 @@ Last updated: 2026-02-19 (session 59)
- **v0.11.8:** ✅ COMPLETE — Per-App Cross-Drive Backup (3-2-1 rule): rsync/restic to secondary drive, deploy page UI, backup page summary, scheduler jobs, API endpoints - **v0.11.8:** ✅ COMPLETE — Per-App Cross-Drive Backup (3-2-1 rule): rsync/restic to secondary drive, deploy page UI, backup page summary, scheduler jobs, API endpoints
- **v0.11.9:** ✅ COMPLETE — UI Polish Fixes: spacing, tooltip on "Módszer", status dot instead of disabled checkbox, progressive disclosure, emoji cleanup - **v0.11.9:** ✅ COMPLETE — UI Polish Fixes: spacing, tooltip on "Módszer", status dot instead of disabled checkbox, progressive disclosure, emoji cleanup
- **First app deployed:** Paperless-ngx on demo-felhom.eu (2026-02-13) - **First app deployed:** Paperless-ngx on demo-felhom.eu (2026-02-13)
- **Running on:** demo-felhom (N100 mini PC) at 192.168.0.162:8080 - **Running on:** demo-felhom (N100 mini PC) at 192.168.0.162:8080, felhotest (remote VPS) at router.abonet.hu:33022
- **All Phase 1-5 features working:** deploy, start/stop/restart/update, logs, health-aware states, auth, monitoring, backups, backup detail page, system monitoring page, settings page - **All Phase 1-5 features working:** deploy, start/stop/restart/update, logs, health-aware states, auth, monitoring, backups, backup detail page, system monitoring page, settings page
## Architecture decisions ## Architecture decisions
+6 -3
View File
@@ -4,7 +4,7 @@
A single, lightweight Go container that replaces Portainer + scattered systemd scripts with a unified, Hungarian-language web dashboard for managing Docker Compose stacks, backups, storage, monitoring, and notifications on customer hardware. A single, lightweight Go container that replaces Portainer + scattered systemd scripts with a unified, Hungarian-language web dashboard for managing Docker Compose stacks, backups, storage, monitoring, and notifications on customer hardware.
**Current version: v0.32.5** **Current version: v0.32.6**
--- ---
@@ -472,7 +472,7 @@ The storage subsystem handles the full lifecycle of external storage: detection,
- `ScanDisks()` uses `lsblk -J -b` for block device enumeration - `ScanDisks()` uses `lsblk -J -b` for block device enumeration
- System disk detection via host fstab parsing (`/host-fstab`) + UUID resolution via `blkid` - System disk detection via host fstab parsing (`/host-fstab`) + UUID resolution via `blkid`
- Partitions enriched with filesystem type, UUID, and label from direct `blkid` probing (Docker containers have incomplete udev cache) - Partitions enriched with filesystem type, UUID, and label from direct `blkid` probing (Docker containers have incomplete udev cache)
- Returns `AvailableDisks` (non-system, non-loop, non-CDROM) and `SystemDisks` separately - Returns `AvailableDisks` (non-system, non-loop, non-CDROM), `SystemDisks`, and `FormatablePartitions` (empty partitions on system disks that are safe to format)
- Handles NVMe (`nvme0n1p1`), SCSI (`sdb1`), and eMMC (`mmcblk0p1`) naming - Handles NVMe (`nvme0n1p1`), SCSI (`sdb1`), and eMMC (`mmcblk0p1`) naming
#### Disk Initialization Wizard (`internal/storage/format.go`) #### Disk Initialization Wizard (`internal/storage/format.go`)
@@ -488,6 +488,8 @@ A step-by-step UI at `/settings/storage/init`:
Safety guards: system disk detection, mount path conflict check, confirmation required, progress channel for real-time UI feedback. Safety guards: system disk detection, mount path conflict check, confirmation required, progress channel for real-time UI feedback.
**System-disk partition formatting:** When the system disk has an empty partition (no filesystem, not mounted, not used for /, /boot, /boot/efi, or swap), the init wizard detects it via `FormatablePartitions` in the scan result and offers to format just that partition. Uses `IsSystemPartition()` (granular per-partition check via fstab) instead of `IsSystemDisk()` (whole-disk block), so sda1 can be formatted while sda3 (root) remains protected.
#### Attach Existing Drive Wizard (`internal/storage/attach.go`) #### Attach Existing Drive Wizard (`internal/storage/attach.go`)
A step-by-step UI at `/settings/storage/attach` for drives that already have a filesystem (e.g., a previously used ext4 drive). Unlike the init wizard, this does **not** format the drive — existing data is preserved. A step-by-step UI at `/settings/storage/attach` for drives that already have a filesystem (e.g., a previously used ext4 drive). Unlike the init wizard, this does **not** format the drive — existing data is preserved.
@@ -1785,7 +1787,8 @@ See `docker-compose.yml` for the full volume configuration.
| Node | Hardware | Domain | Status | | Node | Hardware | Domain | Status |
|------|----------|--------|--------| |------|----------|--------|--------|
| demo-felhom | Acemagic GK3PLUS N100, 16G RAM, 512G SSD + 1TB HDD | demo-felhom.eu | Controller v0.22.3 | | demo-felhom | Acemagic GK3PLUS N100, 16G RAM, 512G SSD + 1TB HDD | demo-felhom.eu | Active |
| felhotest | Remote VPS, 200G + 100G disk | router.abonet.hu:33022 | Active |
| pi-customer-1 | Raspberry Pi 3B+, 1G RAM, 32G SD | pi-customer-1.local | Not yet tested | | pi-customer-1 | Raspberry Pi 3B+, 1G RAM, 32G SD | pi-customer-1.local | Not yet tested |
## Related Repositories ## Related Repositories
@@ -61,6 +61,8 @@ func FormatAndMount(req FormatRequest, progress chan<- FormatProgress) (string,
return "", fail("validating", "Az eszköz nem létezik: "+req.DevicePath, err) return "", fail("validating", "Az eszköz nem létezik: "+req.DevicePath, err)
} }
if req.CreatePartition {
// Whole-disk operation: block if any partition on this disk is a system partition.
isSystem, err := IsSystemDisk(req.DevicePath) isSystem, err := IsSystemDisk(req.DevicePath)
if err != nil { if err != nil {
return "", fail("validating", "Rendszermeghajtó ellenőrzése sikertelen", err) return "", fail("validating", "Rendszermeghajtó ellenőrzése sikertelen", err)
@@ -68,6 +70,17 @@ func FormatAndMount(req FormatRequest, progress chan<- FormatProgress) (string,
if isSystem { if isSystem {
return "", fail("validating", "Ez a rendszermeghajtó — nem formázható!", fmt.Errorf("device is system disk")) return "", fail("validating", "Ez a rendszermeghajtó — nem formázható!", fmt.Errorf("device is system disk"))
} }
} else {
// Partition-only operation: block only if THIS specific partition is a system partition.
// Allows formatting empty partitions on the system disk (e.g., dedicated data partition).
isSysPart, err := IsSystemPartition(req.DevicePath)
if err != nil {
return "", fail("validating", "Rendszerpartíció ellenőrzése sikertelen", err)
}
if isSysPart {
return "", fail("validating", "Ez rendszerpartíció — nem formázható!", fmt.Errorf("partition is a system partition"))
}
}
mounted, err := IsDeviceMounted(req.DevicePath) mounted, err := IsDeviceMounted(req.DevicePath)
if err != nil { if err != nil {
@@ -5,6 +5,7 @@ package storage
import ( import (
"fmt" "fmt"
"os" "os"
"os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"syscall" "syscall"
@@ -46,6 +47,68 @@ func IsSystemDisk(devicePath string) (bool, error) {
return rootDiskGroup == devDiskGroup, nil return rootDiskGroup == devDiskGroup, nil
} }
// IsSystemPartition checks if a specific partition is a system partition
// (/, /boot, /boot/efi, swap) or is currently mounted.
// Unlike IsSystemDisk() which blocks the entire disk, this checks only the
// individual partition — allowing non-system partitions on system disks to be formatted.
func IsSystemPartition(partitionPath string) (bool, error) {
fstabPath := "/host-fstab"
if _, err := os.Stat(fstabPath); err != nil {
fstabPath = "/etc/fstab"
}
data, err := os.ReadFile(fstabPath)
if err != nil {
// If we can't read fstab, err on the side of caution
return true, fmt.Errorf("cannot read fstab: %w", err)
}
systemMounts := map[string]bool{"/": true, "/boot": true, "/boot/efi": true}
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]
if !systemMounts[mountPoint] && fsType != "swap" {
continue
}
var devPath string
if strings.HasPrefix(source, "UUID=") {
uuid := strings.TrimPrefix(source, "UUID=")
if out, err := exec.Command("blkid", "-U", uuid).Output(); err == nil {
devPath = strings.TrimSpace(string(out))
}
} else if strings.HasPrefix(source, "/dev/") {
devPath = source
}
if devPath == partitionPath {
return true, nil
}
}
// Also check if the partition is currently mounted
mounted, err := IsDeviceMounted(partitionPath)
if err != nil {
return false, err
}
if mounted {
return true, nil
}
return false, nil
}
// IsDeviceMounted checks if a device or any of its partitions is currently mounted. // IsDeviceMounted checks if a device or any of its partitions is currently mounted.
func IsDeviceMounted(devicePath string) (bool, error) { func IsDeviceMounted(devicePath string) (bool, error) {
data, err := os.ReadFile("/proc/mounts") data, err := os.ReadFile("/proc/mounts")
@@ -8,6 +8,10 @@ func IsSystemDisk(devicePath string) (bool, error) {
return false, fmt.Errorf("storage init is only supported on Linux") return false, fmt.Errorf("storage init is only supported on Linux")
} }
func IsSystemPartition(partitionPath string) (bool, error) {
return false, fmt.Errorf("storage init is only supported on Linux")
}
func IsDeviceMounted(devicePath string) (bool, error) { func IsDeviceMounted(devicePath string) (bool, error) {
return false, fmt.Errorf("storage init is only supported on Linux") return false, fmt.Errorf("storage init is only supported on Linux")
} }
+10
View File
@@ -25,8 +25,18 @@ type Partition struct {
MountPoint string // "" if not mounted MountPoint string // "" if not mounted
} }
// FormatablePartition is an empty partition on a system disk that can be formatted.
type FormatablePartition struct {
Partition
ParentDiskName string // "sda"
ParentDiskPath string // "/dev/sda"
ParentDiskModel string // "Samsung SSD 870"
ParentDiskSize string // "500 GB"
}
// ScanResult from disk detection. // ScanResult from disk detection.
type ScanResult struct { type ScanResult struct {
AvailableDisks []BlockDevice // Unmounted, non-system disks AvailableDisks []BlockDevice // Unmounted, non-system disks
SystemDisks []BlockDevice // System/mounted disks (display only) SystemDisks []BlockDevice // System/mounted disks (display only)
FormatablePartitions []FormatablePartition // Empty partitions on system disks, safe to format
} }
+76 -1
View File
@@ -164,6 +164,58 @@ func getSystemDiskNames() map[string]bool {
return systemDisks return systemDisks
} }
// getSystemPartitionPaths returns the set of partition device paths (e.g., "/dev/sda3")
// that are system partitions (/, /boot, /boot/efi, swap).
// Unlike getSystemDiskNames() which returns parent disk names, this returns the actual
// partition paths for granular checks.
func getSystemPartitionPaths() map[string]bool {
sysPartitions := map[string]bool{}
fstabPath := "/host-fstab"
if _, err := os.Stat(fstabPath); err != nil {
fstabPath = "/etc/fstab"
}
data, err := os.ReadFile(fstabPath)
if err != nil {
return sysPartitions
}
systemMounts := map[string]bool{"/": true, "/boot": true, "/boot/efi": true}
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]
if !systemMounts[mountPoint] && fsType != "swap" {
continue
}
if strings.HasPrefix(source, "UUID=") {
uuid := strings.TrimPrefix(source, "UUID=")
if out, err := exec.Command("blkid", "-U", uuid).Output(); err == nil {
devPath := strings.TrimSpace(string(out))
if devPath != "" {
sysPartitions[devPath] = true
}
}
} else if strings.HasPrefix(source, "/dev/") {
sysPartitions[source] = true
}
}
return sysPartitions
}
// partitionToParentDisk extracts the parent disk name from a partition device path. // partitionToParentDisk extracts the parent disk name from a partition device path.
// "/dev/sda2" → "sda", "/dev/nvme0n1p2" → "nvme0n1", "/dev/mmcblk0p1" → "mmcblk0" // "/dev/sda2" → "sda", "/dev/nvme0n1p2" → "nvme0n1", "/dev/mmcblk0p1" → "mmcblk0"
func partitionToParentDisk(devPath string) string { func partitionToParentDisk(devPath string) string {
@@ -344,8 +396,31 @@ func ScanDisks(logger *log.Logger, debug bool) (*ScanResult, error) {
enrichWithBlkid(result.AvailableDisks, logger, debug) enrichWithBlkid(result.AvailableDisks, logger, debug)
enrichWithBlkid(result.SystemDisks, logger, debug) enrichWithBlkid(result.SystemDisks, logger, debug)
// Detect formatable partitions on system disks:
// empty (no filesystem), unmounted, and NOT a system partition.
sysPartitionPaths := getSystemPartitionPaths()
for _, sysDisk := range result.SystemDisks {
for _, part := range sysDisk.Partitions {
if part.FSType != "" || part.MountPoint != "" {
continue
}
if sysPartitionPaths[part.Path] {
continue
}
result.FormatablePartitions = append(result.FormatablePartitions, FormatablePartition{
Partition: part,
ParentDiskName: sysDisk.Name,
ParentDiskPath: sysDisk.Path,
ParentDiskModel: sysDisk.Model,
ParentDiskSize: sysDisk.Size,
})
dbg("formatable partition found: %s on system disk %s", part.Path, sysDisk.Name)
}
}
if logger != nil { if logger != nil {
logger.Printf("[INFO] [storage] Found %d disks", len(result.AvailableDisks)+len(result.SystemDisks)) logger.Printf("[INFO] [storage] Found %d disks, %d formatable partitions",
len(result.AvailableDisks)+len(result.SystemDisks), len(result.FormatablePartitions))
} }
dbg("disk scan completed in %s", time.Since(scanStart).Round(time.Millisecond)) dbg("disk scan completed in %s", time.Since(scanStart).Round(time.Millisecond))
@@ -218,6 +218,7 @@ func (s *Server) storageScanAPIHandler(w http.ResponseWriter, r *http.Request) {
"available": result.AvailableDisks, "available": result.AvailableDisks,
"system": result.SystemDisks, "system": result.SystemDisks,
"available_count": len(result.AvailableDisks), "available_count": len(result.AvailableDisks),
"formatable_partitions": result.FormatablePartitions,
}) })
} }
@@ -146,11 +146,15 @@ function scanDisks() {
function renderScanResult(data) { function renderScanResult(data) {
var availEl = document.getElementById('available-disks'); var availEl = document.getElementById('available-disks');
var sysEl = document.getElementById('system-disks-note'); var sysEl = document.getElementById('system-disks-note');
var hasAvail = data.available && data.available.length > 0;
var hasFP = data.formatable_partitions && data.formatable_partitions.length > 0;
if (!data.available || data.available.length === 0) { if (!hasAvail && !hasFP) {
availEl.innerHTML = '<div class="empty-state" style="padding:1rem">Nem található inicializálható meghajtó.</div>'; availEl.innerHTML = '<div class="empty-state" style="padding:1rem">Nem található inicializálható meghajtó vagy partíció.</div>';
} else { } else {
var html = '<h4 style="margin-bottom:.75rem">Talált meghajtók (' + data.available.length + '):</h4>'; var html = '';
if (hasAvail) {
html += '<h4 style="margin-bottom:.75rem">Talált meghajtók (' + data.available.length + '):</h4>';
data.available.forEach(function(disk) { data.available.forEach(function(disk) {
var partInfo = ''; var partInfo = '';
if (disk.Partitions && disk.Partitions.length > 0) { if (disk.Partitions && disk.Partitions.length > 0) {
@@ -159,7 +163,7 @@ function renderScanResult(data) {
}).join(', '); }).join(', ');
} }
html += '<div class="storage-path-item" style="cursor:pointer;border:2px solid transparent" ' + html += '<div class="storage-path-item" style="cursor:pointer;border:2px solid transparent" ' +
'onclick="selectDisk(this, \'' + disk.Path + '\', ' + JSON.stringify(disk.CreatePartition !== false) + ')" ' + 'onclick="selectDisk(this, \'' + disk.Path + '\', true)" ' +
'data-path="' + disk.Path + '" id="disk-' + disk.Name + '">' + 'data-path="' + disk.Path + '" id="disk-' + disk.Name + '">' +
'<div class="storage-path-header"><div class="storage-path-info">' + '<div class="storage-path-header"><div class="storage-path-info">' +
'<span class="storage-path-label">○ ' + disk.Path + ' — ' + (disk.Size || '?') + '</span>' + '<span class="storage-path-label">○ ' + disk.Path + ' — ' + (disk.Size || '?') + '</span>' +
@@ -167,6 +171,28 @@ function renderScanResult(data) {
(partInfo ? '<span class="form-hint">' + partInfo + '</span>' : '<span class="form-hint">Nincs partíció</span>') + (partInfo ? '<span class="form-hint">' + partInfo + '</span>' : '<span class="form-hint">Nincs partíció</span>') +
'</div></div></div>'; '</div></div></div>';
}); });
}
if (hasFP) {
html += '<h4 style="margin-bottom:.75rem;margin-top:1.5rem">Formázható partíciók a rendszermeghajtón (' +
data.formatable_partitions.length + '):</h4>';
html += '<div class="alert alert-info" style="margin-bottom:.75rem;font-size:.85rem">' +
'Az alábbi partíciók a rendszermeghajtón találhatók, de nincsenek használatban. ' +
'Formázás után adattárolóként használhatók.</div>';
data.formatable_partitions.forEach(function(fp) {
var parentInfo = fp.ParentDiskPath + ' (' + fp.ParentDiskSize + ')';
if (fp.ParentDiskModel) parentInfo += ' — ' + fp.ParentDiskModel;
html += '<div class="storage-path-item" style="cursor:pointer;border:2px solid transparent" ' +
'onclick="selectDisk(this, \'' + fp.Path + '\', false)" ' +
'data-path="' + fp.Path + '">' +
'<div class="storage-path-header"><div class="storage-path-info">' +
'<span class="storage-path-label">○ ' + fp.Path + ' — ' + (fp.Size || '?') + '</span>' +
'<span class="form-hint">Rendszermeghajtó partíciója: ' + parentInfo + '</span>' +
'<span class="form-hint">Nincs fájlrendszer — formázásra kész</span>' +
'</div></div></div>';
});
}
availEl.innerHTML = html; availEl.innerHTML = html;
} }
@@ -192,6 +218,20 @@ function selectDisk(el, path, needsPartition) {
document.getElementById('create-partition').value = needsPartition ? 'true' : 'false'; document.getElementById('create-partition').value = needsPartition ? 'true' : 'false';
document.getElementById('selected-device-display').textContent = path; document.getElementById('selected-device-display').textContent = path;
// Update warning text based on whole-disk vs partition-only operation
var warningEl = document.querySelector('#wizard-configure .alert-error');
if (needsPartition) {
warningEl.innerHTML = '<strong>⚠️ FIGYELEM:</strong> A meghajtó <strong>ÖSSZES</strong> adata törlődik!<br>' +
'Ez a művelet <strong>NEM vonható vissza.</strong>';
} else {
warningEl.innerHTML = '<strong>⚠️ FIGYELEM:</strong> A partíció formázva lesz, a rajta lévő adatok törlődnek!<br>' +
'A rendszermeghajtó többi partíciója <strong>NEM</strong> érintett.';
}
// Show/hide the partitioning progress step
var partStep = document.getElementById('pstep-partitioning');
if (partStep) partStep.style.display = needsPartition ? '' : 'none';
// Show configure step // Show configure step
document.getElementById('wizard-configure').style.display = 'block'; document.getElementById('wizard-configure').style.display = 'block';
document.getElementById('wizard-configure').scrollIntoView({behavior:'smooth'}); document.getElementById('wizard-configure').scrollIntoView({behavior:'smooth'});