v0.56.0: Phase 4 — FileBrowser scoping + deploy DB-on-SSD note + monitoring descriptions

4A: scope FileBrowser bind to <drive>/appdata (recovery units + Tier 2 copies under
backups/ are no longer mounted into FileBrowser — customer can't browse/delete the
thing that restores them). 4B: deploy storage-selection step states the chosen drive
holds files while the DB runs on the fast internal SSD + is backed up with the app.
4C: buildStorageBars stable sort + purpose description on the monitoring storage list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-13 13:35:43 +02:00
parent 88ca1178ae
commit 476a97376f
4 changed files with 46 additions and 3 deletions
+18
View File
@@ -1,5 +1,23 @@
## Changelog ## Changelog
### v0.56.0 — Phase 4: FileBrowser scoping + deploy DB-on-SSD note + monitoring storage descriptions (2026-06-13)
Polish layer closing the slice.
- **4A FileBrowser scoping (safety):** the FileBrowser bind mount is now scoped to each drive's
`appdata/` subtree (`<drive>/appdata:/srv/<name>`) instead of the whole drive root. The recovery
units + Tier 2 copies under `backups/` are therefore **not mounted into FileBrowser at all** — the
customer browses their userdata but cannot reach (or even see) the thing that restores them. The
appdata dir is `mkdir`-ed before the bind so the source exists. (`syncFileBrowserMounts`.)
- **4B Deploy-UI communication:** the storage-selection step now states plainly (Hungarian) that the
chosen drive holds the app's **files**, while its **database runs on the fast internal SSD** and is
backed up alongside the app — so "the DB is on the SSD" stops being a surprise. (`deploy.html`.)
- **4C Monitoring storage list:** `buildStorageBars` now sorts deterministically (by path) and carries a
**purpose description** explaining the user-data drives (rendered on the monitoring "Tárolók
kapacitása" list). Note: this list is the controller's registered user-data drives only (the agent's
local/local-lvm/pbs storage is not in this registry), so the role-tier sort/`local`-vs-`local-lvm`
descriptions belong to the agent-backed storage-management page, not here.
### v0.55.0 — Phase 3: auto off-drive Tier 2 (rootfs-headroom guard, durable off-disk target) (2026-06-13) ### v0.55.0 — Phase 3: auto off-drive Tier 2 (rootfs-headroom guard, durable off-disk target) (2026-06-13)
Tier 2 = an **off-drive copy** of each HDD app's recovery unit + bulk userdata to a **different physical Tier 2 = an **off-drive copy** of each HDD app's recovery unit + bulk userdata to a **different physical
+23 -3
View File
@@ -8,6 +8,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"sort"
"strings" "strings"
"time" "time"
@@ -31,13 +32,21 @@ var protectedStackSubdomains = map[string]string{
type StorageBarInfo struct { type StorageBarInfo struct {
Label string // e.g., "USB HDD 1TB", "SYS Storage 350G" Label string // e.g., "USB HDD 1TB", "SYS Storage 350G"
Path string // e.g., "/mnt/hdd_1" Path string // e.g., "/mnt/hdd_1"
Purpose string // Hungarian explanation of what this drive holds (monitoring page)
TotalGB float64 TotalGB float64
UsedGB float64 UsedGB float64
Percent float64 Percent float64
Disconnected bool Disconnected bool
} }
// buildStorageBars returns usage bars for all registered storage paths. // storageBarPurpose is the Hungarian description for the registered user-data drives shown in the
// monitoring "Tárolók kapacitása" list. These are all external/user-data drives (the agent's
// system/PBS storage is not in the controller's storage-path registry), matching the user-data
// purpose text on the storage-management page (Phase 4C).
const storageBarPurpose = "Külső adattároló — a telepített alkalmazások nagy méretű fájljai (média, dokumentumok) ide kerülnek; az adatbázisok a belső SSD-n vannak."
// buildStorageBars returns usage bars for all registered storage paths, in a stable order
// (by path) with a purpose description.
func (s *Server) buildStorageBars() []StorageBarInfo { func (s *Server) buildStorageBars() []StorageBarInfo {
var bars []StorageBarInfo var bars []StorageBarInfo
for _, sp := range s.settings.GetStoragePaths() { for _, sp := range s.settings.GetStoragePaths() {
@@ -49,6 +58,7 @@ func (s *Server) buildStorageBars() []StorageBarInfo {
bars = append(bars, StorageBarInfo{ bars = append(bars, StorageBarInfo{
Label: sp.Label, Label: sp.Label,
Path: sp.Path, Path: sp.Path,
Purpose: storageBarPurpose,
Disconnected: true, Disconnected: true,
}) })
continue continue
@@ -60,11 +70,14 @@ func (s *Server) buildStorageBars() []StorageBarInfo {
bars = append(bars, StorageBarInfo{ bars = append(bars, StorageBarInfo{
Label: sp.Label, Label: sp.Label,
Path: sp.Path, Path: sp.Path,
Purpose: storageBarPurpose,
TotalGB: di.TotalGB, TotalGB: di.TotalGB,
UsedGB: di.UsedGB, UsedGB: di.UsedGB,
Percent: di.UsedPercent, Percent: di.UsedPercent,
}) })
} }
// Deterministic order regardless of registry insertion order.
sort.Slice(bars, func(i, j int) bool { return bars[i].Path < bars[j].Path })
return bars return bars
} }
@@ -1369,11 +1382,18 @@ func (s *Server) syncFileBrowserMounts(resetDBOnChange bool) {
return return
} }
// Build volume mount lines // Build volume mount lines. SCOPE to the drive's `appdata/` subtree only (Phase 4A): the customer
// browses their userdata, but the recovery units + Tier 2 copies under `backups/` are NOT mounted
// into FileBrowser at all — so the thing that restores them can't be browsed or (even read-only)
// surfaced. mkdir the appdata dir first so the bind source exists with sane ownership.
var storageMounts []string var storageMounts []string
for _, sp := range paths { for _, sp := range paths {
mountName := filepath.Base(sp.Path) // "/mnt/hdd_1" → "hdd_1" mountName := filepath.Base(sp.Path) // "/mnt/hdd_1" → "hdd_1"
line := fmt.Sprintf(" - %s:/srv/%s", sp.Path, mountName) appdataSrc := filepath.Join(sp.Path, "appdata")
if err := os.MkdirAll(appdataSrc, 0755); err != nil {
s.logger.Printf("[WARN] [web] FileBrowser: could not ensure appdata dir %s: %v", appdataSrc, err)
}
line := fmt.Sprintf(" - %s:/srv/%s", appdataSrc, mountName)
storageMounts = append(storageMounts, line) storageMounts = append(storageMounts, line)
} }
@@ -538,6 +538,10 @@
<div id="storage-space-warn" class="form-hint" style="color:var(--yellow);display:none"> <div id="storage-space-warn" class="form-hint" style="color:var(--yellow);display:none">
⚠️ A kiválasztott tárhely majdnem megtelt. ⚠️ A kiválasztott tárhely majdnem megtelt.
</div> </div>
<div class="form-hint" style="margin-top:.4rem;opacity:.8">
️ A kiválasztott meghajtón az alkalmazás <strong>fájljai</strong> (média, dokumentumok) tárolódnak.
Az <strong>adatbázis a gyors belső SSD-n</strong> fut — és az alkalmazással együtt készül róla biztonsági mentés.
</div>
{{else}} {{else}}
<input type="text" id="field-{{.EnvVar}}" name="{{.EnvVar}}" <input type="text" id="field-{{.EnvVar}}" name="{{.EnvVar}}"
class="form-control" value="{{.Default}}" class="form-control" value="{{.Default}}"
@@ -106,6 +106,7 @@
<div class="system-bar"> <div class="system-bar">
<div class="system-bar-fill {{usageColor .Percent | printf "system-bar-%s"}}" style="width:{{printf "%.1f" .Percent}}%"></div> <div class="system-bar-fill {{usageColor .Percent | printf "system-bar-%s"}}" style="width:{{printf "%.1f" .Percent}}%"></div>
</div> </div>
{{if .Purpose}}<div class="storage-purpose" style="font-size:.72rem;opacity:.65;margin-top:.2rem">{{.Purpose}}</div>{{end}}
</div> </div>
{{end}} {{end}}
{{end}} {{end}}