diff --git a/TASK.md b/TASK.md index dd17be2..e8dcd95 100644 --- a/TASK.md +++ b/TASK.md @@ -1,377 +1,372 @@ -# TASK: Bugfix — Storage Initialization (FormatAndMount) +# TASK: FileBrowser Auto-Mount on New Storage + UI Polish (3 fixes) -**Version:** 0.11.4 → 0.11.5 -**Priority:** Fix all 3 bugs + add safety improvements before testing format again. - -## Context - -Storage initialization wizard (scan → select disk → format → mount → register) works -up to the partitioning step. Three bugs prevent completion. Fix all three in one pass. - -**Test environment:** demo-felhom.eu, `/dev/sdb` = 931.5 GB USB HDD (HD710 PRO), -has existing GPT partition table with one partition `sdb1` (no filesystem). +**Version:** 0.11.6 +**Scope:** Go handler + template/CSS changes --- -## Bug 1: sfdisk fails with "unsupported command" (CURRENT BLOCKER) +## Feature: Auto-update FileBrowser mounts when storage paths change -### Error output -``` -Old situation: Device Start End Sectors Size Type -/host-dev/sdb1 2048 1953523711 1953521664 931.5G Linux filesystem ->>> Script header accepted. ->>> line 2: unsupported command -Hiba: exit status 1 +### Context + +FileBrowser Quantum is the infrastructure file manager deployed at `files.`. +It's how customers access their files on external drives via the web. + +**Current problem:** FileBrowser's docker-compose.yml has hardcoded volume mounts +pointing to `${HDD_PATH}/storage`, `${HDD_PATH}/media`, `${HDD_PATH}/Dokumentumok`. +When a new drive is initialized via the storage wizard, FileBrowser doesn't get access. +The user has to manually edit the compose — which defeats the managed appliance model. + +**Current FileBrowser compose** (`/opt/docker/stacks/filebrowser/docker-compose.yml`): +```yaml +services: + filebrowser: + image: gtstef/filebrowser:latest + container_name: filebrowser + restart: unless-stopped + environment: + - TZ=Europe/Budapest + volumes: + - filebrowser_data:/home/filebrowser/data + - ${HDD_PATH}/storage:/srv/storage:ro + - ${HDD_PATH}/media:/srv/media + - ${HDD_PATH}/Dokumentumok:/srv/Dokumentumok + # ...traefik labels etc... ``` -### Root cause -Two issues in `format_linux.go` line ~10225: +**Goal:** After a new storage path is registered (or removed), the controller +automatically regenerates FileBrowser's compose with all registered storage paths +as volume mounts, then recreates the container. -```go -sfdiskInput := "label: gpt\n,,,L\n" -cmd := exec.Command("sfdisk", HostDevicePath(req.DevicePath)) -``` +### New mount layout -1. **`,,,L` type shorthand fails on GPT** — this sfdisk version doesn't accept `L` as type - for GPT disklabel. For GPT, sfdisk needs the full GUID or no type (defaults to Linux filesystem). -2. **No `--force` flag** — sdb already has a GPT table with sdb1. sfdisk tries to apply the - script as a delta to the existing layout, not as a fresh layout. -3. **No `wipefs` before sfdisk** — existing partition signatures confuse sfdisk. - -### Fix in `controller/internal/storage/format_linux.go` - -Find this block (around line 10222–10230): - -```go - if req.CreatePartition { - send("partitioning", "Partíció létrehozása...", 15) - - sfdiskInput := "label: gpt\n,,,L\n" - cmd := exec.Command("sfdisk", HostDevicePath(req.DevicePath)) - cmd.Stdin = strings.NewReader(sfdiskInput) - if out, err := cmd.CombinedOutput(); err != nil { - return "", fail("partitioning", "Partícionálás sikertelen: "+string(out), err) - } -``` - -Replace with: - -```go - if req.CreatePartition { - send("partitioning", fmt.Sprintf("wipefs -a %s ...", HostDevicePath(req.DevicePath)), 12) - - // Wipe existing partition table and filesystem signatures first - _ = exec.Command("wipefs", "-a", HostDevicePath(req.DevicePath)).Run() - time.Sleep(500 * time.Millisecond) - - // Create GPT with single partition spanning whole disk - // ",," = start=default, size=default(fill disk), type=default(Linux filesystem GUID) - // --force: overwrite even if device appears busy - // --wipe always: wipe filesystem signatures from newly created partitions - send("partitioning", fmt.Sprintf("sfdisk --force --wipe always %s ...", HostDevicePath(req.DevicePath)), 15) - sfdiskInput := "label: gpt\n,,\n" - cmd := exec.Command("sfdisk", "--force", "--wipe", "always", HostDevicePath(req.DevicePath)) - cmd.Stdin = strings.NewReader(sfdiskInput) - if out, err := cmd.CombinedOutput(); err != nil { - return "", fail("partitioning", "Partícionálás sikertelen: "+string(out), err) - } -``` - ---- - -## Bug 2: `mount mountPath` will fail (NEXT BLOCKER after Bug 1) - -### Current code (around line 10288–10290) - -```go - if out, err := exec.Command("mount", mountPath).CombinedOutput(); err != nil { - return "", fail("mounting", "Csatlakoztatás sikertelen: "+string(out), err) - } -``` - -### Root cause - -`mount /mnt/hdd_1` works by looking up `/mnt/hdd_1` in the process's `/etc/fstab` to find -which device to mount. But inside the container, `/etc/fstab` is Docker's auto-generated fstab -(not the host's). The UUID entry was written to `/host-fstab` (the host's real fstab). - -So `mount /mnt/hdd_1` will fail with "can't find /mnt/hdd_1 in /etc/fstab" or similar. - -### Fix in `controller/internal/storage/format_linux.go` - -Find this line (around line 10288): - -```go - if out, err := exec.Command("mount", mountPath).CombinedOutput(); err != nil { -``` - -Replace with: - -```go - // Mount by device path explicitly — container's /etc/fstab != host fstab, - // so "mount /mnt/hdd_1" (fstab lookup) won't work. - send("mounting", fmt.Sprintf("mount -t ext4 %s %s ...", HostDevicePath(partDev), mountPath), 70) - if out, err := exec.Command("mount", "-t", "ext4", "-o", "defaults,noatime", - HostDevicePath(partDev), mountPath).CombinedOutput(); err != nil { -``` - -The fstab entry in `/host-fstab` still ensures persistence across host reboots. -This explicit mount handles the immediate "mount it right now" operation. - ---- - -## Bug 3: Mount namespace isolation — mount won't be visible on host (RESTART BLOCKER) - -### Root cause - -Even with `privileged: true`, `mount` inside a container operates in the container's -mount namespace. The host kernel does NOT see the mount. Consequences: -- After controller container restart, the mount is gone -- Other containers can't access `/mnt/hdd_1` -- The bind mount `- /mnt:/mnt:rw` shares existing host mounts INTO the container, - but new mounts created inside the container don't propagate BACK to the host - -### Fix: Change `/mnt` volume to use `rshared` mount propagation - -#### 3a. `controller/docker-compose.yml` - -Find this line: +Instead of mounting specific subdirectories, mount each registered storage path +as a top-level directory: ```yaml - # All external storage — /mnt/* for multi-storage + restore - - /mnt:/mnt:rw + volumes: + - filebrowser_data:/home/filebrowser/data + # Auto-generated storage mounts: + - /mnt/hdd_1:/srv/hdd_1 # "Külső HDD 1TB" + - /mnt/hdd_2:/srv/hdd_2 # (if second drive added later) ``` -Replace with: - -```yaml - # All external storage — rshared propagation so mounts created inside - # the container (disk init) propagate to the host and vice versa - - type: bind - source: /mnt - target: /mnt - bind: - propagation: rshared +The user sees in FileBrowser: +``` +/srv/ + hdd_1/ + Dokumentumok/ + media/ + storage/ + hdd_2/ + ... ``` -**Important:** This uses Docker Compose long-form volume syntax. The rest of the volumes -can stay in short form. Only `/mnt` needs propagation. +Each storage path's subdirectories (created during init: `storage/`, `Dokumentumok/`, `media/`) +are visible as subdirectories under the mount name. -#### 3b. `scripts/docker-setup.sh` — Add mount propagation setup +### Implementation -Find the section where the script does final setup steps (after Docker installation, -before or after compose generation). Add: +#### 1. New function: `syncFileBrowserMounts()` in `handlers.go` + +Add a method on `*Server` that regenerates FileBrowser's compose from the current +registered storage paths: + +```go +// syncFileBrowserMounts regenerates FileBrowser's docker-compose.yml +// with volume mounts for all registered storage paths, then recreates the container. +func (s *Server) syncFileBrowserMounts() { + composePath := "/opt/docker/stacks/filebrowser/docker-compose.yml" + + // Check if FileBrowser stack exists + if _, err := os.Stat(composePath); os.IsNotExist(err) { + s.logger.Printf("[WARN] FileBrowser stack not found at %s — skipping mount sync", composePath) + return + } + + // Get all active storage paths + paths := s.settings.GetStoragePaths() // all, not just schedulable + + // Read domain from .env + envPath := "/opt/docker/stacks/filebrowser/.env" + domain := "" + if data, err := os.ReadFile(envPath); err == nil { + for _, line := range strings.Split(string(data), "\n") { + if strings.HasPrefix(line, "DOMAIN=") { + domain = strings.TrimPrefix(line, "DOMAIN=") + break + } + } + } + if domain == "" { + s.logger.Printf("[WARN] Cannot read DOMAIN from FileBrowser .env — skipping mount sync") + return + } + + // Build volume mount lines + var storageMounts []string + for _, sp := range paths { + mountName := filepath.Base(sp.Path) // "/mnt/hdd_1" → "hdd_1" + // Mount each storage path as /srv/ + line := fmt.Sprintf(" - %s:/srv/%s", sp.Path, mountName) + storageMounts = append(storageMounts, line) + } + + // Generate compose from template + compose := generateFileBrowserCompose(domain, storageMounts) + + // Write compose + if err := os.WriteFile(composePath, []byte(compose), 0644); err != nil { + s.logger.Printf("[ERROR] Failed to write FileBrowser compose: %v", err) + return + } + + // Recreate container + cmd := exec.Command("docker", "compose", "up", "-d", "--remove-orphans") + cmd.Dir = filepath.Dir(composePath) + if out, err := cmd.CombinedOutput(); err != nil { + s.logger.Printf("[ERROR] Failed to recreate FileBrowser: %s — %v", string(out), err) + } else { + s.logger.Printf("[INFO] FileBrowser mounts synced — %d storage path(s)", len(paths)) + } +} +``` + +#### 2. Compose template function + +```go +func generateFileBrowserCompose(domain string, storageMounts []string) string { + storageSection := "" + if len(storageMounts) > 0 { + storageSection = "\n # Storage paths (auto-generated by felhom-controller)\n" + + strings.Join(storageMounts, "\n") + } + + return fmt.Sprintf(`# FileBrowser Quantum — Infrastructure file manager +# Domain: files.%s +# Deployed by docker-setup.sh — managed by felhom-controller +# WARNING: Volume mounts are auto-generated. Manual edits will be overwritten. + +services: + filebrowser: + image: gtstef/filebrowser:latest + container_name: filebrowser + restart: unless-stopped + environment: + - TZ=Europe/Budapest + volumes: + - filebrowser_data:/home/filebrowser/data%s + networks: + - traefik-public + deploy: + resources: + limits: + memory: 256M + healthcheck: + test: ["CMD", "wget", "--spider", "-q", "http://localhost:80/"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 15s + labels: + - "traefik.enable=true" + - "traefik.http.routers.filebrowser.rule=Host(` + "`" + `files.%s` + "`" + `)" + - "traefik.http.routers.filebrowser.entrypoints=websecure" + - "traefik.http.routers.filebrowser.tls=true" + - "traefik.http.services.filebrowser.loadbalancer.server.port=80" + - "traefik.docker.network=traefik-public" + +volumes: + filebrowser_data: + +networks: + traefik-public: + external: true +`, domain, storageSection, domain) +} +``` + +**Note on Go string templating:** The backticks in the Traefik label make raw string +literals tricky. You can either: +- Use `fmt.Sprintf` with concatenation for the backtick part (shown above) +- Or use `text/template` with a separate template string +- Either approach is fine — choose whichever is cleaner in the actual code + +#### 3. Call syncFileBrowserMounts() after storage changes + +In `storageInitAPIHandler` (handlers.go), after `AddStoragePath` succeeds, +add the sync call inside the goroutine: + +```go + if err := s.settings.AddStoragePath(sp); err != nil { + s.logger.Printf("[WARN] Failed to register storage path after init: %v", err) + } else { + s.logger.Printf("[INFO] Storage path registered: %s (%s)", mountPath, label) + // Sync FileBrowser mounts with new storage path + s.syncFileBrowserMounts() + } +``` + +Also call it in any handler that removes or modifies storage paths: +- `removeStoragePath` handler (if exists) +- `setDefaultStoragePath` handler — this one only changes the default flag, + does NOT need a FileBrowser sync (mounts don't change) + +#### 4. Handle edge case: FileBrowser not deployed + +The `syncFileBrowserMounts` function already checks if the compose file exists. +If FileBrowser isn't deployed yet, it logs a warning and returns. This is correct — +the user might initialize storage before deploying FileBrowser. + +When FileBrowser IS later deployed (via catalog or docker-setup.sh), it uses `${HDD_PATH}` +from `.env`. The controller could also call `syncFileBrowserMounts()` after deploying +FileBrowser, but that's a separate enhancement. For now, the sync runs on storage changes. + +#### 5. Also preserve .felhom.yml and .env + +`syncFileBrowserMounts()` only writes `docker-compose.yml`. The `.felhom.yml` (metadata) +and `.env` files should NOT be touched — they're managed separately. + +The generated compose does NOT use `${HDD_PATH}` or `${DOMAIN}` env vars — it bakes in +the actual values (domain from .env, paths from settings). This is intentional: it makes +the compose self-contained and debuggable. No hidden env var expansion. + +### Test plan ```bash -# Enable shared mount propagation on /mnt (required for controller disk init) -# This allows mounts created inside the controller container to propagate to the host -log_info "Configuring mount propagation for /mnt..." -mount --make-rshared /mnt 2>/dev/null || mount --make-shared /mnt 2>/dev/null || true -``` +# 1. Before: check current FileBrowser mounts +docker inspect filebrowser --format '{{range .Mounts}}{{.Source}}→{{.Destination}} {{end}}' -**Also** add a comment near the controller compose generation (if any) explaining this requirement. +# 2. Initialize a new drive (or use already-initialized hdd_1) +# After init completes, check compose was rewritten: +cat /opt/docker/stacks/filebrowser/docker-compose.yml +# Should show: - /mnt/hdd_1:/srv/hdd_1 -If `docker-setup.sh` doesn't generate the controller compose, just add the `mount --make-rshared` -to the node preparation section. It's idempotent and safe to run multiple times. +# 3. Verify FileBrowser was recreated +docker inspect filebrowser --format '{{range .Mounts}}{{.Source}}→{{.Destination}} {{end}}' +# Should include /mnt/hdd_1 → /srv/hdd_1 ---- - -## Safety improvement 1: Post-mount verification - -### What -After mount succeeds (exit code 0), verify the mount is actually visible. - -### Where -In `format_linux.go`, right after the mount command succeeds and BEFORE the -`send("mounting", "Csatlakoztatva..."` line, add: - -```go - // Verify mount actually worked (don't just trust exit code) - verifyOut, verifyErr := exec.Command("findmnt", "-n", "-o", "SOURCE", "--target", mountPath).Output() - if verifyErr != nil || strings.TrimSpace(string(verifyOut)) == "" { - return "", fail("mounting", "A csatlakoztatás nem ellenőrizhető: a mount parancs sikerült, de a meghajtó nem látható a rendszerben", fmt.Errorf("mount point %s not found after mount", mountPath)) - } +# 4. Open files. — navigate to /srv/hdd_1/ +# Should see: Dokumentumok/, media/, storage/ ``` --- -## Safety improvement 2: Use ASCII mount name for ext4 filesystem label +## UI Fix 1: "Nincs csatolva!" badge → "Rendszermeghajtón" -### What -The current code uses `req.Label` (user-provided display label like "Külső HDD 1TB") for the -ext4 `-L` label. ext4 labels are limited to 16 BYTES. Hungarian UTF-8 chars (ű, ó, é) are -2 bytes each, so "Külső HDD 1TB" could exceed the limit or get truncated mid-character. +### Problem +`/mnt/hdd_placeholder` shows a red "Nincs csatolva!" badge. It's on the system SSD, +which isn't a separate mount point. This looks like an error but it's just informational. ### Where -In `format_linux.go`, find the label preparation block (around line 10249–10254): +`controller/internal/web/templates/settings.html` — find the badge rendering for +storage paths that aren't mount points. Look for "Nincs csatolva!" text. -```go - label := req.Label - if label == "" { - label = req.MountName - } - if len(label) > 16 { - label = label[:16] - } +### Fix +Change from red/danger badge to yellow/warning badge with new text: + +**Before:** +```html +Nincs csatolva! ``` -Replace with: - -```go - // Use ASCII-safe mount name for ext4 filesystem label (16-byte limit). - // The display label (req.Label) stays in settings.json for the UI. - fsLabel := req.MountName - if len(fsLabel) > 16 { - fsLabel = fsLabel[:16] - } +**After:** +```html +Rendszermeghajtón ``` -Then update the mkfs.ext4 call right below to use `fsLabel` instead of `label`: - -```go - mkfsCmd := exec.Command("mkfs.ext4", "-L", fsLabel, "-F", HostDevicePath(partDev)) +Add badge style if `badge-warn` doesn't exist in `style.css`: +```css +.badge-warn { + background: rgba(250, 204, 21, 0.15); + color: #facc15; +} ``` --- -## Safety improvement 3: Smart partition handling (skip repartition when unnecessary) +## UI Fix 2: Init progress bar — remove disk usage zone gradient -### What -The scan shows sdb has 1 partition (sdb1) with no filesystem. The JS always sends -`CreatePartition: true` (because `disk.CreatePartition` is undefined on the `BlockDevice` -struct, so `undefined !== false` evaluates to `true` in JS). - -For a disk that already has exactly one partition with no filesystem, we should skip -the destructive repartition step and just format the existing partition directly. +### Problem +The storage init progress bar uses the disk-usage bar style which has +green→yellow→red gradient zones. At 30% progress it looks alarming. A task +progress bar should be a simple single-color fill on neutral background. ### Where -In `handlers.go`, in `storageInitAPIHandler`, AFTER building `fmtReq` (around line 14175–14180) -and BEFORE the `go func()` goroutine, add: +`controller/internal/web/templates/storage_init.html` — find the progress bar. +`controller/internal/web/templates/style.css` — add new class. -```go - // Smart partition: if device is a whole disk with exactly 1 partition - // with no filesystem, skip repartitioning — just format existing partition - if fmtReq.CreatePartition { - result, scanErr := storage.ScanDisks() - if scanErr == nil { - for _, disk := range result.AvailableDisks { - if disk.Path == req.DevicePath && len(disk.Partitions) == 1 && disk.Partitions[0].FSType == "" { - s.logger.Printf("[INFO] Disk %s has 1 empty partition (%s) — skipping repartition", - req.DevicePath, disk.Partitions[0].Path) - fmtReq.DevicePath = disk.Partitions[0].Path // e.g., "/dev/sdb1" - fmtReq.CreatePartition = false - break - } - } - } - } +### Fix +Add a new CSS class for task progress (not disk usage): + +```css +.progress-bar-task { + height: 8px; + background: var(--border); + border-radius: 4px; + overflow: hidden; + width: 100%; +} + +.progress-bar-task .progress-fill { + height: 100%; + background: var(--accent); + border-radius: 4px; + transition: width 0.3s ease; +} ``` -This way, for demo sdb (which has sdb1 with no FS), it will: -1. Set DevicePath to `/dev/sdb1` -2. Set CreatePartition to `false` -3. Skip wipefs + sfdisk entirely -4. Go straight to `mkfs.ext4 /host-dev/sdb1` +In `storage_init.html`, replace the current progress bar element (which reuses the +disk usage bar class) with: -**Note:** The wipefs+sfdisk fix (Bug 1) is still needed as fallback for truly -unpartitioned disks or disks with multiple/incompatible partitions. +```html +
+
+
+0% +``` + +Update the JS `pollProgress` function to set the width and text: +```js +document.getElementById('progress-fill').style.width = data.percent + '%'; +document.getElementById('progress-percent').textContent = data.percent + '%'; +``` --- -## Safety improvement 4: Descriptive progress messages +## UI Fix 3: "Alapértelmezett" button → "Legyen alapértelmezett" -### What -Include executed command details in progress messages for remote debugging. -The progress messages show in the UI and get logged by the handler. +### Problem +Button text "Alapértelmezett" reads as a status label ("Default"), not an action. +Users think the drive IS the default, not that clicking MAKES it the default. ### Where -Throughout `format_linux.go`, update the `send()` calls to include command info. -Examples already shown in the Bug 1 and Bug 2 fixes above. Also update: +`controller/internal/web/templates/settings.html` — find the button for setting +a non-default storage path as default. -For the mkfs step: -```go - send("formatting", fmt.Sprintf("mkfs.ext4 -L %s -F %s ...", fsLabel, HostDevicePath(partDev)), 30) -``` +### Fix +Change button text from "Alapértelmezett" to "Legyen alapértelmezett". -For the blkid step (around line 10274): -```go - send("mounting", fmt.Sprintf("UUID lekérése: blkid %s ...", HostDevicePath(partDev)), 65) -``` +Verify the distinction is clear: +- **IS default:** Green badge "Alapértelmezett" (no button, status indicator) +- **NOT default:** Button "Legyen alapértelmezett" (action) --- ## Summary: All changes by file -### `controller/internal/storage/format_linux.go` (5 changes) +| File | Change | Type | +|------|--------|------| +| `handlers.go` | New `syncFileBrowserMounts()` + `generateFileBrowserCompose()` | Feature | +| `handlers.go` | Call `syncFileBrowserMounts()` after `AddStoragePath` | Feature | +| `settings.html` | Red "Nincs csatolva!" → yellow "Rendszermeghajtón" | UI fix | +| `settings.html` | "Alapértelmezett" button → "Legyen alapértelmezett" | UI fix | +| `storage_init.html` | Progress bar: zone gradient → simple fill bar | UI fix | +| `style.css` | Add `.badge-warn` and `.progress-bar-task` classes | UI fix | -1. Partition block: Add `wipefs -a`; change sfdisk input `",,,L"` → `",,"`; - add `--force --wipe always` flags -2. Mount block: Change `mount mountPath` → `mount -t ext4 -o defaults,noatime HostDevicePath(partDev) mountPath` -3. After mount: Add `findmnt` verification -4. Label: Use `req.MountName` (ASCII) instead of `req.Label` (UTF-8) for `mkfs.ext4 -L` -5. Progress messages: Include command details in `send()` calls - -### `controller/docker-compose.yml` (1 change) - -6. Change `/mnt:/mnt:rw` to long-form syntax with `propagation: rshared` - -### `controller/internal/web/handlers.go` (1 change) - -7. In `storageInitAPIHandler`: Add smart partition detection before launching goroutine - -### `scripts/docker-setup.sh` (1 change) - -8. Add `mount --make-rshared /mnt` to node preparation section - ---- - -## Build & deploy procedure - -```bash -# 1. On the host FIRST (before restarting controller): -sudo mount --make-rshared /mnt - -# 2. Build new image with fixes (normal build process) - -# 3. Deploy -cd /opt/docker/felhom-controller -sudo docker compose up -d - -# 4. Verify container sees /host-dev -docker exec felhom-controller ls /host-dev/sd* - -# 5. Verify rshared propagation is active -docker inspect felhom-controller --format '{{range .Mounts}}{{if eq .Destination "/mnt"}}Propagation={{.Propagation}}{{end}}{{end}}' -# Should show: Propagation=rshared - -# 6. Test storage init wizard: -# - Scan → sdb appears -# - Select sdb → configure hdd_1 → type FORMÁZÁS -# - Watch progress panel — should show command details -# - Should complete successfully - -# 7. Verify mount on HOST (proves propagation): -findmnt /mnt/hdd_1 -# Should show /dev/sdb1 mounted at /mnt/hdd_1 - -# 8. Verify fstab entry: -grep hdd_1 /etc/fstab -# Should show UUID=... /mnt/hdd_1 ext4 defaults,nofail,noatime 0 2 - -# 9. Verify storage registered in settings: -# Visit Settings page → Adattárolók → /mnt/hdd_1 should appear - -# 10. Restart controller — verify mount survives: -docker restart felhom-controller -docker exec felhom-controller ls /mnt/hdd_1/ -# Should show: storage/ Dokumentumok/ -``` - ---- - -## What NOT to change - -- **Dockerfile** — packages already correct (fdisk, e2fsprogs, util-linux, rsync, parted) -- **scan_linux.go** — scan works correctly after v0.11.1 fixes -- **safety_linux.go / safety.go** — system disk detection works -- **Template/JS** — wizard UI works fine; `CreatePartition` default-true is handled in handler \ No newline at end of file +### What NOT to change +- `.felhom.yml` for FileBrowser — unchanged +- `.env` for FileBrowser — unchanged (domain read from it, not written) +- `docker-setup.sh` FileBrowser install function — still works for initial install; + controller takes over compose management after first storage init +- Go storage package — no changes \ No newline at end of file