FileBrowser Auto-Mount on New Storage + UI Polish (3 fixes)
This commit is contained in:
@@ -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.<domain>`.
|
||||
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`
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
**Also** add a comment near the controller compose generation (if any) explaining this requirement.
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
## 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:
|
||||
Add a method on `*Server` that regenerates FileBrowser's compose from the current
|
||||
registered storage paths:
|
||||
|
||||
```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))
|
||||
// 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
|
||||
|
||||
## Safety improvement 2: Use ASCII mount name for ext4 filesystem label
|
||||
|
||||
### 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.
|
||||
|
||||
### Where
|
||||
In `format_linux.go`, find the label preparation block (around line 10249–10254):
|
||||
|
||||
```go
|
||||
label := req.Label
|
||||
if label == "" {
|
||||
label = req.MountName
|
||||
}
|
||||
if len(label) > 16 {
|
||||
label = label[:16]
|
||||
}
|
||||
```
|
||||
|
||||
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]
|
||||
}
|
||||
```
|
||||
|
||||
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))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Safety improvement 3: Smart partition handling (skip repartition when unnecessary)
|
||||
|
||||
### 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.
|
||||
|
||||
### Where
|
||||
In `handlers.go`, in `storageInitAPIHandler`, AFTER building `fmtReq` (around line 14175–14180)
|
||||
and BEFORE the `go func()` goroutine, add:
|
||||
|
||||
```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
|
||||
// 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/<name>
|
||||
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))
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
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`
|
||||
#### 2. Compose template function
|
||||
|
||||
**Note:** The wipefs+sfdisk fix (Bug 1) is still needed as fallback for truly
|
||||
unpartitioned disks or disks with multiple/incompatible partitions.
|
||||
```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
|
||||
# 1. Before: check current FileBrowser mounts
|
||||
docker inspect filebrowser --format '{{range .Mounts}}{{.Source}}→{{.Destination}} {{end}}'
|
||||
|
||||
# 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
|
||||
|
||||
# 3. Verify FileBrowser was recreated
|
||||
docker inspect filebrowser --format '{{range .Mounts}}{{.Source}}→{{.Destination}} {{end}}'
|
||||
# Should include /mnt/hdd_1 → /srv/hdd_1
|
||||
|
||||
# 4. Open files.<domain> — navigate to /srv/hdd_1/
|
||||
# Should see: Dokumentumok/, media/, storage/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Safety improvement 4: Descriptive progress messages
|
||||
## UI Fix 1: "Nincs csatolva!" badge → "Rendszermeghajtón"
|
||||
|
||||
### 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
|
||||
`/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
|
||||
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 badge rendering for
|
||||
storage paths that aren't mount points. Look for "Nincs csatolva!" text.
|
||||
|
||||
For the mkfs step:
|
||||
```go
|
||||
send("formatting", fmt.Sprintf("mkfs.ext4 -L %s -F %s ...", fsLabel, HostDevicePath(partDev)), 30)
|
||||
### Fix
|
||||
Change from red/danger badge to yellow/warning badge with new text:
|
||||
|
||||
**Before:**
|
||||
```html
|
||||
<span class="badge badge-danger">Nincs csatolva!</span>
|
||||
```
|
||||
|
||||
For the blkid step (around line 10274):
|
||||
```go
|
||||
send("mounting", fmt.Sprintf("UUID lekérése: blkid %s ...", HostDevicePath(partDev)), 65)
|
||||
**After:**
|
||||
```html
|
||||
<span class="badge badge-warn">Rendszermeghajtón</span>
|
||||
```
|
||||
|
||||
Add badge style if `badge-warn` doesn't exist in `style.css`:
|
||||
```css
|
||||
.badge-warn {
|
||||
background: rgba(250, 204, 21, 0.15);
|
||||
color: #facc15;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UI Fix 2: Init progress bar — remove disk usage zone gradient
|
||||
|
||||
### 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
|
||||
`controller/internal/web/templates/storage_init.html` — find the progress bar.
|
||||
`controller/internal/web/templates/style.css` — add new class.
|
||||
|
||||
### 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;
|
||||
}
|
||||
```
|
||||
|
||||
In `storage_init.html`, replace the current progress bar element (which reuses the
|
||||
disk usage bar class) with:
|
||||
|
||||
```html
|
||||
<div class="progress-bar-task">
|
||||
<div class="progress-fill" id="progress-fill" style="width: 0%"></div>
|
||||
</div>
|
||||
<span id="progress-percent" style="margin-left: 0.5rem; font-size: 0.9rem; color: var(--text-dim)">0%</span>
|
||||
```
|
||||
|
||||
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 + '%';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UI Fix 3: "Alapértelmezett" button → "Legyen alapértelmezett"
|
||||
|
||||
### 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
|
||||
`controller/internal/web/templates/settings.html` — find the button for setting
|
||||
a non-default storage path as default.
|
||||
|
||||
### Fix
|
||||
Change button text from "Alapértelmezett" to "Legyen alapértelmezett".
|
||||
|
||||
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
|
||||
### 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
|
||||
Reference in New Issue
Block a user