Files
deploy-felhom-compose/TASK.md
T

6.5 KiB
Raw Blame History

TASK.md — Cross-Drive Backup Validation Fix (v0.12.5)

Prompt (copy-paste this into Claude Code)

Read TASK.md for the full plan. Apply all code changes described, then build and deploy.
After all fixes are done:
1. Run `go build ./...` and `go vet ./...` from the controller/ directory — fix any errors
2. Update CHANGELOG.md with a new entry at the top (session 41, v0.12.5)
3. Commit, build, and deploy following the workflow in CLAUDE.md

Context

The cross-drive backup for Immich failed last night with:

Hiba: destination /mnt/hdd_placeholder is not a mount point (0s)

Root cause: ValidateDestination() in crossdrive.go hard-blocked non-mount-point destinations. The /mnt/hdd_placeholder folder is on the internal SSD (not a separate mount), so the device-ID check in IsMountPoint() returned false.

Already fixed (in current working tree): The mount-point check was changed from a hard block to a logged warning (lines 172174 of crossdrive.go). The backup will now proceed for system-drive destinations.

Remaining work: Improve the disk space validation to be smarter about system-drive destinations (don't fill up the OS drive).


Fix 1: Smarter space checks in ValidateDestination (crossdrive.go)

File: internal/backup/crossdrive.go, lines 161183

Current code (already patched with the mount-point warning):

func (r *CrossDriveRunner) ValidateDestination(path string) error {
	if path == "" {
		return fmt.Errorf("destination path is empty")
	}
	if _, err := os.Stat(path); os.IsNotExist(err) {
		return fmt.Errorf("destination %s does not exist", path)
	}
	if !system.IsMountPoint(path) {
		r.logger.Printf("[WARN] Destination %s is not a separate mount point (system drive) — backup will proceed but data is not protected against drive failure", path)
	}
	if !system.IsWritable(path) {
		return fmt.Errorf("destination %s is not writable", path)
	}
	di := system.GetDiskUsage(path)
	if di != nil && di.AvailGB < 0.1 {
		return fmt.Errorf("destination %s has insufficient free space (%.1f GB)", path, di.AvailGB)
	}
	return nil
}

Required change: Replace the flat 100MB space check (lines 178181) with drive-type-aware logic:

func (r *CrossDriveRunner) ValidateDestination(path string) error {
	if path == "" {
		return fmt.Errorf("destination path is empty")
	}
	if _, err := os.Stat(path); os.IsNotExist(err) {
		return fmt.Errorf("destination %s does not exist", path)
	}
	onSystemDrive := !system.IsMountPoint(path)
	if onSystemDrive {
		r.logger.Printf("[WARN] Destination %s is not a separate mount point (system drive) — backup will proceed but data is not protected against drive failure", path)
	}
	if !system.IsWritable(path) {
		return fmt.Errorf("destination %s is not writable", path)
	}
	if di := system.GetDiskUsage(path); di != nil {
		if onSystemDrive {
			// System drive: protect OS stability — require ≥10 GB free and <90% used
			if di.AvailGB < 10 {
				return fmt.Errorf("destination %s is on the system drive with only %.1f GB free — at least 10 GB required to protect OS stability", path, di.AvailGB)
			}
			if di.UsedPercent >= 90 {
				return fmt.Errorf("destination %s is on the system drive at %.0f%% capacity — maximum 90%% allowed", path, di.UsedPercent)
			}
		} else {
			// External drive: just ensure it's not completely full
			if di.AvailGB < 0.1 {
				return fmt.Errorf("destination %s has insufficient free space (%.1f GB free)", path, di.AvailGB)
			}
		}
	}
	return nil
}

Update the function comment (lines 161164) to match:

// ValidateDestination checks that the destination path exists, is writable,
// and has sufficient free space. System-drive destinations get stricter limits
// (≥10 GB free, <90% used) to protect OS stability; external drives just need
// ≥100 MB. Non-mount-point destinations are allowed with a logged warning.

Fix 2: Align CheckBackupDestination thresholds for system drives (mounts_linux.go)

File: internal/system/mounts_linux.go, lines 134186

The web UI's CheckBackupDestination currently applies the same disk thresholds (90% warn, 95% block) regardless of drive type. For system drives, it should use the same stricter thresholds as the runner (90% block, 10 GB minimum) so the UI warning matches what the runner will actually enforce.

Required change: In the Tier 4 block (lines 171183), add system-drive-specific checks BEFORE the generic percentage checks. The logic should be:

	// Tier 4: disk usage checks
	if di := GetDiskUsage(path); di != nil {
		h.UsedPercent = di.UsedPercent
		h.FreeGB = di.AvailGB
		if h.SystemDrive {
			// System drive: stricter limits to protect OS stability
			if di.AvailGB < 10 {
				h.Warning = fmt.Sprintf("A rendszermeghajtón csak %.1f GB szabad — legalább 10 GB szükséges a rendszer stabilitásához!", di.AvailGB)
				h.Blocked = true
				h.Severity = "critical"
			} else if di.UsedPercent >= 90 {
				h.Warning = fmt.Sprintf("A rendszermeghajtó %.0f%%-ban megtelt — maximum 90%% megengedett.", di.UsedPercent)
				h.Blocked = true
				h.Severity = "critical"
			}
			// If neither triggers, keep the Tier 3 system-drive warning
		} else {
			// External drive: original thresholds
			if di.UsedPercent >= 95 {
				h.Warning = fmt.Sprintf("A mentési meghajtó megtelt (%.0f%% használt)!", di.UsedPercent)
				h.Blocked = true
				h.Severity = "critical"
			} else if di.UsedPercent >= 90 {
				h.Warning = fmt.Sprintf("A mentési meghajtó majdnem megtelt (%.0f%% használt).", di.UsedPercent)
				h.Severity = "warning"
			}
		}
	}

Note: the else if di.UsedPercent >= 90 && h.Severity == "ok" condition in the original was preventing the 90% warning from overriding the system-drive warning. The new code separates the branches cleanly — system drive gets its own block, external drive gets its own.


Summary of thresholds

Condition System drive External drive
Free space < 10 GB Block
Usage ≥ 90% Block Warning
Usage ≥ 95% (caught by 90%) Block
Free space < 100 MB (caught by 10GB) Block

Files to modify

  1. internal/backup/crossdrive.goValidateDestination() (Fix 1)
  2. internal/system/mounts_linux.goCheckBackupDestination() (Fix 2)

Post-fix checklist

  • go build ./... passes
  • go vet ./... passes
  • Update CHANGELOG.md — session 41, version v0.12.5, describe both fixes
  • Commit, build on 192.168.0.180, deploy on 192.168.0.162
  • Verify with docker ps and docker logs