v0.51.0: offsite-backup UI (felhom-pbs DR) + Model-A double-nest fix
- Backups page: whole-guest backup shown as real DR — target label "Biztonsági szerver – külön hardver (PBS)"; app-data "Távoli mentés" card now reflects the PBS offsite tier (guestBackupView.Offsite) instead of "nincs beállítva". - Model-A double-nest fix: appbackup path helpers take a felhom-data NAMESPACE ROOT (no internal felhom-data join); backup.Manager.namespaceRoot/AppNamespaceRoot resolve HDD-vs-systemDataPath provenance so a drive-resident app's backups land single-nested (<drive>/backups/... on the guest = <drive>/felhom-data/backups/... on the host) instead of .../felhom-data/felhom-data/.... Writes, deletion (GetStackBackupData/RemoveStack/ ProtectedHDDPaths), wipe-warning scan, and export updated coherently; legacy double-nest dirs kept protected. New appbackup test asserts no doubled segment. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,30 @@
|
||||
## Changelog
|
||||
|
||||
### v0.51.0 — offsite-backup UI (felhom-pbs DR) + Model-A double-nest fix (2026-06-12)
|
||||
|
||||
Pairs with felhom-agent v0.28.0 (whole-guest backup re-targeted to the offsite PBS tier).
|
||||
|
||||
**Backups page — the whole-guest backup is now shown as real DR (separate hardware).** The
|
||||
"Rendszermentés" section's target label calls out the offsite tier: `backupTargetLabel` returns
|
||||
**"Biztonsági szerver – külön hardver (PBS)"** for a PBS-stored backup (detected via `backupIsPBS`
|
||||
on the target id / archive volid), so the customer sees the backup survives a host hardware failure.
|
||||
The app-data section's **"Távoli mentés"** card stops reading "nincs beállítva": a new
|
||||
`guestBackupView.Offsite` flag drives it to **"külön hardveren (PBS)"** with a ✓ when the whole-guest
|
||||
backup landed on PBS. The restore-test "Visszaállítás ellenőrizve" trust signal is unchanged.
|
||||
|
||||
**Model-A double-nest fix — drive-resident app backups land single-nested.** Under slice-10 Model A the
|
||||
host agent binds `<drive>/felhom-data` onto the guest mountpoint, so an enrolled drive's in-guest mount
|
||||
IS the felhom-data namespace root (basename need not be `felhom-data`, e.g. `/mnt/felhom-usb`). The
|
||||
backup path helpers were re-prepending `felhom-data`, producing `.../felhom-data/felhom-data/...` on the
|
||||
host. `appbackup` path helpers now take a NAMESPACE ROOT (no internal `felhom-data` join) plus a new
|
||||
`NamespaceRoot(drivePath, inGuestDrive)`; `backup.Manager.namespaceRoot`/`AppNamespaceRoot` resolve
|
||||
provenance (a drive-resident app's mount is the root as-is; only the SSD-only `systemDataPath` fallback
|
||||
appends `felhom-data`). All parallel constructions updated coherently so writes, deletion
|
||||
(`GetStackBackupData`, `RemoveStack` backups-base + `ProtectedHDDPaths` — legacy double-nest dirs kept
|
||||
protected), the wipe-warning secondary scan, and export all agree. `api.router` passes the namespace
|
||||
root across the package boundary. New `appbackup` test asserts no doubled `felhom-data` segment for an
|
||||
in-guest drive and exactly one for the system fallback.
|
||||
|
||||
### v0.50.0 — slice 10 P4: dual-role drives + backup-aware wipe warning (2026-06-12)
|
||||
|
||||
Pairs with felhom-agent P3 (self-heal). Establishes the dual-role MODEL + the backup-aware wipe
|
||||
|
||||
@@ -561,13 +561,15 @@ func (r *Router) getStackBackupData(w http.ResponseWriter, _ *http.Request, name
|
||||
return
|
||||
}
|
||||
|
||||
// Compute the drive path for this stack (HDD or system data path)
|
||||
var drivePath string
|
||||
// Compute the felhom-data namespace root for this stack (drive-resident apps: the in-guest
|
||||
// mount IS the namespace; SSD-only: <systemDataPath>/felhom-data). Passing the namespace root
|
||||
// (not the raw drive) keeps GetStackBackupData's paths single-nested under Model A.
|
||||
var nsRoot string
|
||||
if r.backupMgr != nil {
|
||||
drivePath = r.backupMgr.GetAppDrivePath(name)
|
||||
nsRoot = r.backupMgr.AppNamespaceRoot(name)
|
||||
}
|
||||
|
||||
resp, err := r.stackMgr.GetStackBackupData(name, drivePath)
|
||||
resp, err := r.stackMgr.GetStackBackupData(name, nsRoot)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusNotFound, apiResponse{OK: false, Error: err.Error()})
|
||||
return
|
||||
@@ -598,9 +600,9 @@ func (r *Router) removeStack(w http.ResponseWriter, req *http.Request, name stri
|
||||
// backup has moved to the host agent; only the app-data DB-dump path is removed here.
|
||||
var backupPaths []string
|
||||
if body.RemoveBackups && r.backupMgr != nil {
|
||||
drivePath := r.backupMgr.GetAppDrivePath(name)
|
||||
if drivePath != "" {
|
||||
backupPaths = append(backupPaths, backup.AppDBDumpPath(drivePath, name))
|
||||
nsRoot := r.backupMgr.AppNamespaceRoot(name)
|
||||
if nsRoot != "" {
|
||||
backupPaths = append(backupPaths, backup.AppDBDumpPath(nsRoot, name))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,22 +10,40 @@ import "path/filepath"
|
||||
// FelhomDataDir is the namespace directory on storage drives for all felhom-managed data.
|
||||
const FelhomDataDir = "felhom-data"
|
||||
|
||||
// PrimaryBackupPath returns the root primary backup directory for a drive.
|
||||
func PrimaryBackupPath(drivePath string) string {
|
||||
return filepath.Join(drivePath, FelhomDataDir, "backups", "primary")
|
||||
// NamespaceRoot resolves the felhom-data namespace ROOT for a drive path. All the path helpers
|
||||
// below take this namespace root (the directory that directly contains backups/ and appdata/),
|
||||
// NOT a bare drive path — they do not append felhom-data themselves.
|
||||
//
|
||||
// Model A (slice 10): the host agent binds <drive>/felhom-data onto the guest mountpoint, so an
|
||||
// enrolled user-data drive's IN-GUEST mount already IS the namespace root (and its basename need
|
||||
// NOT be "felhom-data" — e.g. /mnt/felhom-usb). For such mounts pass inGuestDrive=true → the path
|
||||
// is returned as-is, so callers no longer double-nest into .../felhom-data/felhom-data/... .
|
||||
//
|
||||
// For a bare drive root that still holds a felhom-data SUBDIR — the SSD-only system-data fallback,
|
||||
// or any legacy host-side layout — pass inGuestDrive=false → the felhom-data segment is appended.
|
||||
func NamespaceRoot(drivePath string, inGuestDrive bool) string {
|
||||
if inGuestDrive {
|
||||
return filepath.Clean(drivePath)
|
||||
}
|
||||
return filepath.Join(drivePath, FelhomDataDir)
|
||||
}
|
||||
|
||||
// AppDBDumpPath returns the DB dump directory for an app on its home drive.
|
||||
func AppDBDumpPath(drivePath, stackName string) string {
|
||||
return filepath.Join(drivePath, FelhomDataDir, "backups", "primary", stackName, "db-dumps")
|
||||
// PrimaryBackupPath returns the root primary backup directory under a felhom-data namespace root.
|
||||
func PrimaryBackupPath(nsRoot string) string {
|
||||
return filepath.Join(nsRoot, "backups", "primary")
|
||||
}
|
||||
|
||||
// AppVolumeDumpPath returns the directory for Docker volume dump tars on an app's home drive.
|
||||
func AppVolumeDumpPath(drivePath, stackName string) string {
|
||||
return filepath.Join(drivePath, FelhomDataDir, "backups", "primary", stackName, "volume-dumps")
|
||||
// AppDBDumpPath returns the DB dump directory for an app under a felhom-data namespace root.
|
||||
func AppDBDumpPath(nsRoot, stackName string) string {
|
||||
return filepath.Join(nsRoot, "backups", "primary", stackName, "db-dumps")
|
||||
}
|
||||
|
||||
// AppDataDir returns the app data directory path on a drive.
|
||||
func AppDataDir(drivePath, stackName string) string {
|
||||
return filepath.Join(drivePath, FelhomDataDir, "appdata", stackName)
|
||||
// AppVolumeDumpPath returns the Docker-volume dump-tar directory for an app under a namespace root.
|
||||
func AppVolumeDumpPath(nsRoot, stackName string) string {
|
||||
return filepath.Join(nsRoot, "backups", "primary", stackName, "volume-dumps")
|
||||
}
|
||||
|
||||
// AppDataDir returns the app data directory under a felhom-data namespace root.
|
||||
func AppDataDir(nsRoot, stackName string) string {
|
||||
return filepath.Join(nsRoot, "appdata", stackName)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package appbackup
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// slash normalizes OS path separators so the assertions hold on both Linux (production) and Windows
|
||||
// (dev). The double-nest invariant is about path segments, not the separator byte.
|
||||
func slash(p string) string { return filepath.ToSlash(p) }
|
||||
|
||||
// TestNoDoubleFelhomDataForInGuestDrive asserts that for an enrolled in-guest user-data drive
|
||||
// (Model A, slice 10 — the in-guest mount IS the felhom-data namespace root), none of the backup
|
||||
// path helpers re-inject a felhom-data segment, so the on-host path is single-nested rather than
|
||||
// the .../felhom-data/felhom-data/... double-nest that this fix removes.
|
||||
func TestNoDoubleFelhomDataForInGuestDrive(t *testing.T) {
|
||||
// An enrolled drive's in-guest mount — note the basename is NOT "felhom-data".
|
||||
drive := filepath.FromSlash("/mnt/felhom-usb")
|
||||
ns := NamespaceRoot(drive, true)
|
||||
if ns != drive {
|
||||
t.Fatalf("in-guest namespace root should be the drive mount as-is: got %q want %q", ns, drive)
|
||||
}
|
||||
|
||||
paths := map[string]string{
|
||||
"PrimaryBackupPath": PrimaryBackupPath(ns),
|
||||
"AppDBDumpPath": AppDBDumpPath(ns, "nextcloud"),
|
||||
"AppVolumeDumpPath": AppVolumeDumpPath(ns, "nextcloud"),
|
||||
"AppDataDir": AppDataDir(ns, "nextcloud"),
|
||||
}
|
||||
for name, p := range paths {
|
||||
sp := slash(p)
|
||||
if strings.Contains(sp, FelhomDataDir+"/"+FelhomDataDir) {
|
||||
t.Errorf("%s double-nests felhom-data: %q", name, sp)
|
||||
}
|
||||
if n := strings.Count(sp, FelhomDataDir); n > 1 {
|
||||
t.Errorf("%s has %d felhom-data segments (want <=1): %q", name, n, sp)
|
||||
}
|
||||
if !strings.HasPrefix(sp, "/mnt/felhom-usb/") {
|
||||
t.Errorf("%s not rooted at the drive mount: %q", name, sp)
|
||||
}
|
||||
}
|
||||
|
||||
// Concrete expected single-nested DB-dump path for a drive-resident app.
|
||||
if got, want := slash(AppDBDumpPath(ns, "nextcloud")), "/mnt/felhom-usb/backups/primary/nextcloud/db-dumps"; got != want {
|
||||
t.Errorf("drive-resident DB-dump path: got %q want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSystemFallbackNestsOnceUnderFelhomData asserts the SSD-only system-data fallback still nests
|
||||
// under exactly one felhom-data segment (the bare drive root does NOT already contain the namespace).
|
||||
func TestSystemFallbackNestsOnceUnderFelhomData(t *testing.T) {
|
||||
ns := NamespaceRoot(filepath.FromSlash("/mnt/sys_drive"), false)
|
||||
if want := filepath.Join(filepath.FromSlash("/mnt/sys_drive"), FelhomDataDir); ns != want {
|
||||
t.Fatalf("system fallback namespace root: got %q want %q", ns, want)
|
||||
}
|
||||
got := slash(AppDBDumpPath(ns, "nextcloud"))
|
||||
if want := "/mnt/sys_drive/felhom-data/backups/primary/nextcloud/db-dumps"; got != want {
|
||||
t.Errorf("system fallback DB-dump path: got %q want %q", got, want)
|
||||
}
|
||||
if n := strings.Count(got, FelhomDataDir); n != 1 {
|
||||
t.Errorf("system fallback should have exactly one felhom-data segment, got %d: %q", n, got)
|
||||
}
|
||||
}
|
||||
@@ -152,9 +152,11 @@ func diskFree(path string) int64 {
|
||||
return size
|
||||
}
|
||||
|
||||
// ExportDir returns the exports directory on a drive.
|
||||
// ExportDir returns the exports directory on a drive. Model A (slice 10): a registered drive's
|
||||
// in-guest mount IS the felhom-data namespace root, so exports/ sits directly under it (no
|
||||
// felhom-data segment — avoids the .../felhom-data/felhom-data/... double-nest).
|
||||
func ExportDir(drivePath string) string {
|
||||
return filepath.Join(drivePath, "felhom-data", "exports")
|
||||
return filepath.Join(drivePath, "exports")
|
||||
}
|
||||
|
||||
// humanizeBytes converts bytes to human-readable format.
|
||||
|
||||
@@ -87,19 +87,26 @@ func humanizeBytes(b int64) string {
|
||||
}
|
||||
|
||||
// --- function forwarders (paths) ---
|
||||
//
|
||||
// NOTE: the path helpers below take a felhom-data NAMESPACE ROOT, not a bare drive path. Use
|
||||
// NamespaceRoot (or Manager.namespaceRoot / Manager.AppNamespaceRoot) to resolve the root first.
|
||||
|
||||
func PrimaryBackupPath(drivePath string) string {
|
||||
return appbackup.PrimaryBackupPath(drivePath)
|
||||
func NamespaceRoot(drivePath string, inGuestDrive bool) string {
|
||||
return appbackup.NamespaceRoot(drivePath, inGuestDrive)
|
||||
}
|
||||
|
||||
func AppDBDumpPath(drivePath, stackName string) string {
|
||||
return appbackup.AppDBDumpPath(drivePath, stackName)
|
||||
func PrimaryBackupPath(nsRoot string) string {
|
||||
return appbackup.PrimaryBackupPath(nsRoot)
|
||||
}
|
||||
|
||||
func AppVolumeDumpPath(drivePath, stackName string) string {
|
||||
return appbackup.AppVolumeDumpPath(drivePath, stackName)
|
||||
func AppDBDumpPath(nsRoot, stackName string) string {
|
||||
return appbackup.AppDBDumpPath(nsRoot, stackName)
|
||||
}
|
||||
|
||||
func AppDataDir(drivePath, stackName string) string {
|
||||
return appbackup.AppDataDir(drivePath, stackName)
|
||||
func AppVolumeDumpPath(nsRoot, stackName string) string {
|
||||
return appbackup.AppVolumeDumpPath(nsRoot, stackName)
|
||||
}
|
||||
|
||||
func AppDataDir(nsRoot, stackName string) string {
|
||||
return appbackup.AppDataDir(nsRoot, stackName)
|
||||
}
|
||||
|
||||
@@ -93,6 +93,26 @@ func (m *Manager) GetAppDrivePath(stackName string) string {
|
||||
return m.systemDataPath
|
||||
}
|
||||
|
||||
// namespaceRoot maps an app's drive path to its felhom-data namespace ROOT (the dir that directly
|
||||
// holds backups/ and appdata/). A drive-resident app's in-guest mount IS the namespace already
|
||||
// (Model A, slice 10 — the agent binds <drive>/felhom-data onto the guest mountpoint), so it is used
|
||||
// as-is; only the SSD-only system-data fallback gets the felhom-data subdir appended. This is what
|
||||
// keeps a drive-resident app's backups single-nested instead of .../felhom-data/felhom-data/... .
|
||||
func (m *Manager) namespaceRoot(drivePath string) string {
|
||||
return NamespaceRoot(drivePath, drivePath != m.systemDataPath)
|
||||
}
|
||||
|
||||
// AppNamespaceRoot returns the felhom-data namespace root for a stack's keep-side backups, resolving
|
||||
// HDD-vs-system provenance internally. For callers outside this package that only know the stack
|
||||
// name (e.g. the API router) so they don't double-nest the felhom-data segment.
|
||||
func (m *Manager) AppNamespaceRoot(stackName string) string {
|
||||
drivePath := m.GetAppDrivePath(stackName)
|
||||
if drivePath == "" {
|
||||
return ""
|
||||
}
|
||||
return m.namespaceRoot(drivePath)
|
||||
}
|
||||
|
||||
// groupStacksByDrive groups deployed stacks by their home drive path.
|
||||
func (m *Manager) groupStacksByDrive() map[string][]StackSummary {
|
||||
if m.stackProvider == nil {
|
||||
@@ -170,7 +190,7 @@ func (m *Manager) runDBDumpsInternal(ctx context.Context) error {
|
||||
continue
|
||||
}
|
||||
|
||||
dumpDir := AppDBDumpPath(drivePath, db.StackName)
|
||||
dumpDir := AppDBDumpPath(m.namespaceRoot(drivePath), db.StackName)
|
||||
|
||||
result := DumpOne(ctx, db, dumpDir, m.logger, m.isDebug())
|
||||
results = append(results, result)
|
||||
@@ -239,7 +259,7 @@ func (m *Manager) DumpAppVolumes(stackName string) error {
|
||||
return fmt.Errorf("cannot determine drive path for %s", stackName)
|
||||
}
|
||||
|
||||
dumpDir := AppVolumeDumpPath(drivePath, stackName)
|
||||
dumpDir := AppVolumeDumpPath(m.namespaceRoot(drivePath), stackName)
|
||||
if err := os.MkdirAll(dumpDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating volume dump dir: %w", err)
|
||||
}
|
||||
@@ -395,7 +415,7 @@ func (m *Manager) DumpStackDB(ctx context.Context, stackName string) error {
|
||||
if drivePath == "" || !filepath.IsAbs(drivePath) {
|
||||
return fmt.Errorf("cannot determine absolute drive path for %s (systemDataPath not configured?)", stackName)
|
||||
}
|
||||
dumpDir := AppDBDumpPath(drivePath, stackName)
|
||||
dumpDir := AppDBDumpPath(m.namespaceRoot(drivePath), stackName)
|
||||
|
||||
m.logger.Printf("[INFO] [backup] Running pre-backup DB dump for %s (%d database(s)) → %s", stackName, len(stackDBs), dumpDir)
|
||||
|
||||
@@ -428,7 +448,7 @@ func (m *Manager) listAllDumpFiles() []DumpFileInfo {
|
||||
var allFiles []DumpFileInfo
|
||||
for drive, stacks := range m.groupStacksByDrive() {
|
||||
for _, stack := range stacks {
|
||||
dumpDir := AppDBDumpPath(drive, stack.Name)
|
||||
dumpDir := AppDBDumpPath(m.namespaceRoot(drive), stack.Name)
|
||||
if files, err := ListDumpFiles(dumpDir); err == nil {
|
||||
allFiles = append(allFiles, files...)
|
||||
}
|
||||
|
||||
@@ -83,7 +83,7 @@ func (m *Manager) RestoreApp(stackName, snapshotID string) error {
|
||||
|
||||
// restoreDockerVolumes populates Docker volumes from tar files in the volume dump directory.
|
||||
func (m *Manager) restoreDockerVolumes(stackName, drivePath string) error {
|
||||
dumpDir := AppVolumeDumpPath(drivePath, stackName)
|
||||
dumpDir := AppVolumeDumpPath(m.namespaceRoot(drivePath), stackName)
|
||||
entries, err := os.ReadDir(dumpDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
|
||||
@@ -60,12 +60,18 @@ func ProtectedHDDPaths(hddPath string) map[string]bool {
|
||||
return nil
|
||||
}
|
||||
return map[string]bool{
|
||||
// Model A: the in-guest drive mount IS the felhom-data namespace root, so backups/ and
|
||||
// appdata/ sit directly under it (no felhom-data segment).
|
||||
hddPath: true,
|
||||
filepath.Join(hddPath, "appdata"): true,
|
||||
filepath.Join(hddPath, "backups"): true,
|
||||
filepath.Join(hddPath, "media"): true,
|
||||
filepath.Join(hddPath, "Dokumentumok"): true,
|
||||
// Legacy pre-Model-A double-nest location; kept protected so any leftover data there is
|
||||
// never wiped by a removal.
|
||||
filepath.Join(hddPath, felhomDataDir): true,
|
||||
filepath.Join(hddPath, felhomDataDir, "appdata"): true,
|
||||
filepath.Join(hddPath, felhomDataDir, "backups"): true,
|
||||
filepath.Join(hddPath, "media"): true,
|
||||
filepath.Join(hddPath, "Dokumentumok"): true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -383,8 +389,9 @@ func (m *Manager) RemoveStack(name string, removeHDDData bool, backupPathsToRemo
|
||||
}
|
||||
}
|
||||
|
||||
// Step 5: Handle backup data cleanup
|
||||
backupsBase := filepath.Join(hddPath, felhomDataDir, "backups")
|
||||
// Step 5: Handle backup data cleanup. Model A: backups/ sits directly under the namespace-root
|
||||
// drive mount (no felhom-data segment).
|
||||
backupsBase := filepath.Join(hddPath, "backups")
|
||||
if m.isDebug() {
|
||||
m.logger.Printf("[DEBUG] [stacks] RemoveStack %s: processing %d backup paths for removal (base=%s)", name, len(backupPathsToRemove), backupsBase)
|
||||
}
|
||||
@@ -453,12 +460,13 @@ func (m *Manager) GetStackBackupData(name string, drivePath string) (*BackupData
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// Check DB dump directory: <drive>/felhom-data/backups/primary/<stack>/db-dumps
|
||||
dbDumpPath := filepath.Join(drivePath, felhomDataDir, "backups", "primary", name, "db-dumps")
|
||||
// Check DB dump directory. drivePath is the felhom-data namespace ROOT (Model A: the in-guest
|
||||
// drive mount itself), so backups/ sits directly under it: <nsRoot>/backups/primary/<stack>/db-dumps
|
||||
dbDumpPath := filepath.Join(drivePath, "backups", "primary", name, "db-dumps")
|
||||
resp.BackupPaths = append(resp.BackupPaths, buildPathInfo(dbDumpPath))
|
||||
|
||||
// Check cross-drive rsync directory: <drive>/felhom-data/backups/secondary/<stack>/rsync
|
||||
rsyncPath := filepath.Join(drivePath, felhomDataDir, "backups", "secondary", name, "rsync")
|
||||
// Check cross-drive rsync directory: <nsRoot>/backups/secondary/<stack>/rsync
|
||||
rsyncPath := filepath.Join(drivePath, "backups", "secondary", name, "rsync")
|
||||
resp.BackupPaths = append(resp.BackupPaths, buildPathInfo(rsyncPath))
|
||||
|
||||
if m.isDebug() {
|
||||
|
||||
@@ -31,7 +31,8 @@ type guestBackupView struct {
|
||||
Success bool
|
||||
StartedAt time.Time
|
||||
SizeBytes int64
|
||||
Target string // human label: "Biztonsági szerver (PBS)" / "Helyi tároló (local)"
|
||||
Target string // human label: "Biztonsági szerver – külön hardver (PBS)" / "Helyi tároló (local)"
|
||||
Offsite bool // the whole-guest backup landed on the PBS offsite tier (separate hardware)
|
||||
Archive string
|
||||
Mode string // snapshot | stop
|
||||
StopMode bool // mode == stop → full app downtime during the backup (warn)
|
||||
@@ -73,6 +74,7 @@ func (s *Server) loadGuestBackup(ctx context.Context) *guestBackupView {
|
||||
v.Mode = st.Backup.Mode
|
||||
v.StopMode = st.Backup.Mode == "stop"
|
||||
v.Target = backupTargetLabel(st.Backup)
|
||||
v.Offsite = backupIsPBS(st.Backup)
|
||||
if t, perr := time.Parse(time.RFC3339, st.Backup.StartedAt); perr == nil {
|
||||
v.StartedAt = t
|
||||
}
|
||||
@@ -97,13 +99,20 @@ func (s *Server) loadGuestBackup(ctx context.Context) *guestBackupView {
|
||||
return v
|
||||
}
|
||||
|
||||
// backupTargetLabel maps the agent's backup target to a customer-facing Hungarian label, surfacing
|
||||
// whether the backup landed on the PBS offsite tier or local host storage (from the archive volid /
|
||||
// target id — "felhom-pbs"/"pbs:" ⇒ PBS, else local host storage).
|
||||
func backupTargetLabel(b *agentapi.BackupRecord) string {
|
||||
// backupIsPBS reports whether a whole-guest backup landed on the PBS offsite tier (separate
|
||||
// hardware), inferred from the target id / archive volid ("felhom-pbs"/"pbs:" ⇒ PBS).
|
||||
func backupIsPBS(b *agentapi.BackupRecord) bool {
|
||||
id := strings.ToLower(b.TargetID)
|
||||
if strings.Contains(id, "pbs") || strings.HasPrefix(strings.ToLower(b.Archive), "felhom-pbs") || strings.Contains(strings.ToLower(b.Archive), "pbs:") {
|
||||
return "Biztonsági szerver (PBS)"
|
||||
arc := strings.ToLower(b.Archive)
|
||||
return strings.Contains(id, "pbs") || strings.HasPrefix(arc, "felhom-pbs") || strings.Contains(arc, "pbs:")
|
||||
}
|
||||
|
||||
// backupTargetLabel maps the agent's backup target to a customer-facing Hungarian label. The PBS
|
||||
// case calls out that the backup is on SEPARATE HARDWARE (real disaster recovery — survives a host
|
||||
// disk/hardware failure), which is the whole point of re-pointing the backup offsite.
|
||||
func backupTargetLabel(b *agentapi.BackupRecord) string {
|
||||
if backupIsPBS(b) {
|
||||
return "Biztonsági szerver – külön hardver (PBS)"
|
||||
}
|
||||
if b.TargetID != "" {
|
||||
return "Helyi tároló (" + b.TargetID + ")"
|
||||
|
||||
@@ -324,12 +324,13 @@ func (s *Server) handleStorageImpact(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// backupCopiesOnPath lists the apps whose CROSS-DRIVE (secondary) backup copies are stored on the
|
||||
// drive mounted at `where` (slice 10 P4) — the felhom-data/backups/secondary/<app> dirs. A wipe of
|
||||
// this drive removes these copies. Best-effort filesystem scan; empty until the cross-drive backup
|
||||
// ENGINE (a follow-on slice) actually writes here. Shared/aggregate dirs (restic repo, _infra) are
|
||||
// not apps and are skipped.
|
||||
// drive mounted at `where` (slice 10 P4) — the backups/secondary/<app> dirs. A wipe of this drive
|
||||
// removes these copies. Best-effort filesystem scan; empty until the cross-drive backup ENGINE (a
|
||||
// follow-on slice) actually writes here. Shared/aggregate dirs (restic repo, _infra) are not apps
|
||||
// and are skipped. Model A: `where` is the in-guest drive mount, which IS the felhom-data namespace
|
||||
// root, so backups/ sits directly under it (no felhom-data segment — avoids the double-nest).
|
||||
func backupCopiesOnPath(where string) []string {
|
||||
secondary := filepath.Join(where, appbackup.FelhomDataDir, "backups", "secondary")
|
||||
secondary := filepath.Join(appbackup.NamespaceRoot(where, true), "backups", "secondary")
|
||||
entries, err := os.ReadDir(secondary)
|
||||
if err != nil {
|
||||
return nil // no secondary backups here (or the path isn't readable) — nothing to warn about
|
||||
|
||||
@@ -140,10 +140,17 @@
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if and .GuestBackup .GuestBackup.Offsite}}
|
||||
<div class="stat-card stat-running">
|
||||
<div class="stat-value">✓</div>
|
||||
<div class="stat-label">Távoli mentés<br><span class="relative-time">külön hardveren (PBS)</span></div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="stat-card" style="border-left-color: var(--gray);">
|
||||
<div class="stat-value" style="background:var(--gray);-webkit-background-clip:text;background-clip:text;">–</div>
|
||||
<div class="stat-label">Távoli mentés<br><span class="relative-time">nincs beállítva</span></div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
<div class="stat-card stat-total">
|
||||
<div class="stat-value">
|
||||
|
||||
Reference in New Issue
Block a user