v0.318.0: the internal data place, partitions, an honest whole-box backup offer (R-1084), sys_drive never offered (R-1082)
gates / gates (push) Successful in 46s

felhom.eu architecture/16 §4-§6 (build B). MinAgent 0.157.0 — the internal place and the partitions appear only when
agent >= 0.158.0 reports them. Tests red-proved; four parity fixtures refreshed (only the new lines).

Claude-Session: https://claude.ai/code/session_012qRErfCoiTkvDK9N5XHbzb
This commit is contained in:
2026-10-16 03:57:49 +02:00
parent 123d2d553f
commit a43d0423d5
28 changed files with 913 additions and 26 deletions
+84 -8
View File
@@ -37,6 +37,9 @@ type BackupTargetState struct {
// was told the backup was on the system disk (false) and offered the drive that just disappeared
// (felhom.eu audits/E2D-fresh-vm-2026-07-29.md §5.3).
TargetAbsent bool
// OfferUnproven (R-1084) marks an offered drive the agent cannot prove is another disk than the system data:
// the page then makes no disk-failure promise.
OfferUnproven bool
// TargetID is the agent's primary tier storage id (e.g. "felhom-backup" / "local").
TargetID string
// Label is the customer-facing drive name when the target is a real drive.
@@ -95,7 +98,7 @@ func (s *Server) resolveBackupTargetState(ctx context.Context) BackupTargetState
// this state must be correct on its own. Belt here, braces there.
return st
}
st.OfferPath, st.OfferLabel = s.firstOfferableDrive(disks.Disks)
st.OfferPath, st.OfferLabel, st.OfferUnproven = s.firstOfferableDrive(ctx, disks.Disks)
return st
}
@@ -127,20 +130,81 @@ func targetIsConfiguredDrive(targetID string) bool {
// firstOfferableDrive picks a registered, connected, non-network drive that could hold the backup.
// It is a SUGGESTION for the offer — the customer still has to choose (E-2 §3). Nothing here assigns
// anything.
func (s *Server) firstOfferableDrive(disks []agentapi.DiskInfo) (path, label string) {
//
// R-1084 (v0.318.0, felhom.eu architecture/16 §4): the offer promises survival of a DISK failure, so it now asks the
// agent whether the drive is really another disk than the system data. A proven different disk wins; a drive the
// agent cannot prove (a VM, a USB bridge that hides the serial) is still offered, but unproven=true makes the page say
// so in build A's words instead of the promise; a drive on the SAME disk is never offered (a copy there dies with the
// disk); an agent older than 0.157.0 keeps today's offer. Pinned by r1084_offer_identity_test.go.
func (s *Server) firstOfferableDrive(ctx context.Context, disks []agentapi.DiskInfo) (path, label string, unproven bool) {
fallback, fallbackLabel, fallbackUnproven := "", "", false
for _, sp := range s.settings.GetStoragePaths() {
if sp.IsNetwork() || sp.Decommissioned || sp.Disconnected {
if sp.IsNetwork() || sp.IsInternal() || sp.Decommissioned || sp.Disconnected {
continue
}
for _, d := range disks {
// Only a drive the agent classifies as user-data with its OWN mountpoint can be a target;
// the agent refuses anything else anyway, so offering it would be a dead end.
if d.GuestPath == sp.Path && d.MountPath != "" && d.Role == "user-data" {
return sp.Path, s.storageLabelFor(sp.Path)
if d.GuestPath != sp.Path || d.MountPath == "" || d.Role != "user-data" {
continue
}
switch s.offerRelationFor(ctx, sp.Path) {
case offerDifferent:
return sp.Path, s.storageLabelFor(sp.Path), false
case offerSame:
continue
case offerUnknown:
if fallback == "" {
fallback, fallbackLabel, fallbackUnproven = sp.Path, s.storageLabelFor(sp.Path), true
}
default: // offerLegacy: today's behaviour
if fallback == "" {
fallback, fallbackLabel, fallbackUnproven = sp.Path, s.storageLabelFor(sp.Path), false
}
}
}
}
return "", ""
return fallback, fallbackLabel, fallbackUnproven
}
// offerRelation is the agent's verdict on a candidate backup drive against the system data (R-1084).
type offerRelation int
const (
offerLegacy offerRelation = iota // the agent cannot judge (older than 0.157.0): today's offer, today's words
offerDifferent // proven different disks: the disk-failure promise holds
offerUnknown // not proven (a VM, a hiding USB bridge, or the question failed): no promise
offerSame // the same physical disk: never offered
)
// offerRelationFor asks the agent (POST /disks/same-disk) through the sameDiskFn seam (nil → the real client).
func (s *Server) offerRelationFor(ctx context.Context, drive string) offerRelation {
sdp := "/mnt/sys_drive"
if s.cfg != nil && s.cfg.Paths.SystemDataPath != "" {
sdp = s.cfg.Paths.SystemDataPath
}
if s.sameDiskFn != nil {
return s.sameDiskFn(ctx, drive, sdp)
}
client, err := s.agentClient()
if err != nil {
return offerLegacy
}
if client.Supports(ctx, agentapi.FeatureSameDisk) == agentapi.SupportNo {
return offerLegacy
}
r, err := client.SameDisk(ctx, drive, sdp)
switch {
case err != nil:
s.logger.Printf("[WARN] [backup-target] same-disk question for %s failed: %v — the offer makes no promise", drive, err)
return offerUnknown
case r.Proven && r.Different:
return offerDifferent
case r.Proven:
return offerSame
default:
return offerUnknown
}
}
// baseName is path.Base without importing path into this file's surface.
@@ -171,6 +235,9 @@ func baseName(p string) string {
const (
backupTargetDegradedKey = "backup.target.degraded"
backupTargetOfferKey = "backup.target.offer"
// backupTargetOfferUnprovenKey (R-1084, v0.318.0): the offer for a drive the agent cannot prove is another disk —
// build A's words, no disk-failure promise.
backupTargetOfferUnprovenKey = "backup.target.offer_unproven"
// backupTargetAbsentText (R-114) is the CONFIGURED-BUT-GONE state. It is VERBATIM the hub's
// customerMessages["backup_target_absent"] (felhom.eu hub/internal/notify/templates.go:93) so the
// banner a customer reads on the page and the email they receive say exactly the same thing — a
@@ -245,7 +312,11 @@ func (s *Server) backupTargetView(ctx context.Context, lang string) *BackupTarge
}
v := &BackupTargetView{Message: s.msgLang(lang, key)}
if st.OfferPath != "" {
v.OfferPath, v.OfferLabel, v.OfferText = st.OfferPath, st.OfferLabel, s.msgLang(lang, backupTargetOfferKey)
offerKey := backupTargetOfferKey
if st.OfferUnproven {
offerKey = backupTargetOfferUnprovenKey // R-1084: no disk-failure promise for an unproven disk
}
v.OfferPath, v.OfferLabel, v.OfferText = st.OfferPath, st.OfferLabel, s.msgLang(lang, offerKey)
}
return v
}
@@ -264,8 +335,13 @@ func (s *Server) handleBackupTargetState(w http.ResponseWriter, r *http.Request)
out["message"] = s.msg(r, degradedMessageFor(st))
if st.OfferPath != "" {
out["offer_path"] = st.OfferPath
out["offer_unproven"] = st.OfferUnproven // R-1084
out["offer_label"] = st.OfferLabel
out["offer_message"] = s.msg(r, backupTargetOfferKey)
offerKey := backupTargetOfferKey
if st.OfferUnproven {
offerKey = backupTargetOfferUnprovenKey // R-1084
}
out["offer_message"] = s.msg(r, offerKey)
}
}
}