feat(shares): R-7b Parts 4-6 — shares restore, samba liveness, UI truth-up
Part 4 — restore: RestoreSharesScratch + PlaceSharesRestore as SIBLINGS of the
per-app scratch/place pair. Files merged missing-only (never overwriting), each
destination PREFIX-ASSERTED against registered LIVE storage roots; definitions
merged with existing-wins; ReconcileSamba via a seam (backup must not import
stacks); credential restored best-effort into the samba named volume.
New routes POST /backup/shares/{restore,place} + a restore-page entry that renders
'Megosztasok', never the raw reserved key.
Also adds scratchJoin: reconstructing an absolute captured path under a scratch
must strip the volume name rather than rely on filepath.Join.
Part 5 — liveness: EffectiveProtected gains a settings-backed dynamic extra so the
samba CONTAINER (not the stack name — they differ) is watched exactly while sharing
is on. FINDING: the issue -> health 'fail' -> existing health_critical event ->
alert -> Hungarian degradation e-mail path needs NO further change, and introduces
no new event type, so the allowlist gotcha does not apply.
Part 6 — UI: per-tier backup status lines on the Megosztas page (amber only on
deviation). Verified the two warning-prose sites (offbox_capture/tier2_capture)
only ever receive per-app stack names, so no mapping is needed there.
RED-PROOFS RUN AND REVERTED (both fired):
4. prefix-assert removed -> place-guard traversal test FAILS
5. dynamic samba extra removed -> Scenario E enabled-case FAILS
This commit is contained in:
@@ -256,7 +256,7 @@ func (s *Server) debugDump(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// Health
|
||||
healthReport := monitor.RunHealthCheck(s.cfg, s.cpuCollector, storagePaths, s.logger)
|
||||
healthReport := monitor.RunHealthCheck(s.cfg, s.cpuCollector, storagePaths, s.settings.GetSMBSettings(), s.logger)
|
||||
dump["health"] = map[string]interface{}{
|
||||
"status": healthReport.Status,
|
||||
"issues": healthReport.Issues,
|
||||
|
||||
@@ -815,6 +815,14 @@ func (s *Server) backupsRestoreHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
data["OffboxScratchReady"] = ready
|
||||
// R-7b: the shares source is not an app — it has no per-app toggle and no recovery unit — so it
|
||||
// gets its own restore entry rather than a synthetic row in OffboxApps (which would also make it
|
||||
// appear in the per-app offsite TOGGLE list on /backups/remote, where it does not belong).
|
||||
if s.backupMgr != nil {
|
||||
data["SharesRestoreOffered"] = s.settings != nil && len(s.settings.GetSMBShares()) > 0
|
||||
data["SharesScratchReady"] = s.backupMgr.SharesScratchReady()
|
||||
data["SharesDisplayName"] = backup.SharesDisplayName
|
||||
}
|
||||
s.executeTemplate(w, r, "backups_restore", data)
|
||||
}
|
||||
|
||||
|
||||
@@ -355,3 +355,64 @@ func (s *Server) offboxPlaceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
}()
|
||||
offboxRedirectTo(w, r, "/backups/restore", "A helyreállítás elindult — az állapot itt frissül.", false)
|
||||
}
|
||||
|
||||
// --- R-7b: „Megosztások" restore ------------------------------------------------------------------
|
||||
//
|
||||
// A SIBLING of the per-app restore pair above, not a special case of it: the shares source has no
|
||||
// recovery unit and no per-app toggle, so it gets its own two-step flow (restore to scratch, then a
|
||||
// deliberate place-to-live). The display name is always „Megosztások" — the reserved `_shares` key
|
||||
// never reaches a customer-facing surface.
|
||||
|
||||
// sharesRestoreHandler restores the latest shares snapshot into an on-data-drive scratch dir
|
||||
// (POST /backup/shares/restore). Non-destructive: nothing live is touched until the place action.
|
||||
func (s *Server) sharesRestoreHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if s.backupMgr == nil || !s.backupMgr.OffboxConfigured() {
|
||||
offboxRedirectTo(w, r, "/backups/restore", "A távoli mentési cél nincs beállítva.", true)
|
||||
return
|
||||
}
|
||||
if s.backupMgr.IsRunning() {
|
||||
offboxRedirectTo(w, r, "/backups/restore", "Egy mentési/visszaállítási művelet már fut.", true)
|
||||
return
|
||||
}
|
||||
s.backupMgr.BeginRestoreOp("shares-restore", backup.SharesDisplayName)
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||||
defer cancel()
|
||||
if err := s.backupMgr.RestoreSharesScratch(ctx); err != nil {
|
||||
s.logger.Printf("[ERROR] [web] shares restore (async): %v", err)
|
||||
s.backupMgr.EndRestoreOp(false, "A megosztások visszaállítása sikertelen: "+err.Error())
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] shares restore completed (async)")
|
||||
s.backupMgr.EndRestoreOp(true, "A megosztások visszaállítása elkészült — most helyreállíthatod az élő adatok közé.")
|
||||
}()
|
||||
offboxRedirectTo(w, r, "/backups/restore", "A megosztások visszaállítása elindult — az állapot itt frissül.", false)
|
||||
}
|
||||
|
||||
// sharesPlaceHandler merges a completed shares scratch into the live share folders, re-adds the
|
||||
// missing definitions and restores the household credential (POST /backup/shares/place).
|
||||
func (s *Server) sharesPlaceHandler(w http.ResponseWriter, r *http.Request) {
|
||||
if s.backupMgr == nil || !s.backupMgr.OffboxConfigured() {
|
||||
offboxRedirectTo(w, r, "/backups/restore", "A távoli mentési cél nincs beállítva.", true)
|
||||
return
|
||||
}
|
||||
if s.backupMgr.IsRunning() {
|
||||
offboxRedirectTo(w, r, "/backups/restore", "Egy mentési/visszaállítási művelet már fut.", true)
|
||||
return
|
||||
}
|
||||
s.backupMgr.BeginRestoreOp("shares-place", backup.SharesDisplayName)
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
|
||||
defer cancel()
|
||||
res, err := s.backupMgr.PlaceSharesRestore(ctx)
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] [web] shares place (async): %v", err)
|
||||
s.backupMgr.EndRestoreOp(false, "A megosztások helyreállítása sikertelen: "+err.Error())
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] shares place completed (async): %d file(s), %d definition(s)",
|
||||
res.FilesRestored, len(res.DefinitionsAdded))
|
||||
s.backupMgr.EndRestoreOp(true, res.FlashMessage())
|
||||
}()
|
||||
offboxRedirectTo(w, r, "/backups/restore", "A megosztások helyreállítása elindult — az állapot itt frissül.", false)
|
||||
}
|
||||
|
||||
@@ -418,6 +418,11 @@ func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
s.offboxRestoreHandler(w, r)
|
||||
case path == "/backup/offbox/place" && r.Method == http.MethodPost:
|
||||
s.offboxPlaceHandler(w, r)
|
||||
// R-7b: „Megosztások" restore — a sibling of the per-app pair above.
|
||||
case path == "/backup/shares/restore" && r.Method == http.MethodPost:
|
||||
s.sharesRestoreHandler(w, r)
|
||||
case path == "/backup/shares/place" && r.Method == http.MethodPost:
|
||||
s.sharesPlaceHandler(w, r)
|
||||
// Controller-driven escrow ceremony wizard (v0.127.0): the customer-facing R flow.
|
||||
case path == "/backup/escrow" && r.Method == http.MethodGet:
|
||||
s.escrowWizardPageHandler(w, r)
|
||||
|
||||
@@ -154,6 +154,26 @@ func (s *Server) sharingPageData() map[string]interface{} {
|
||||
roots = append(roots, map[string]string{"Path": sp.Path, "Label": label})
|
||||
}
|
||||
data["StorageRoots"] = roots
|
||||
|
||||
// R-7b: per-tier backup truth. Until R-7b the „Felhőmentés" toggle promised a protection the
|
||||
// engines did not deliver; these two lines are what makes the promise checkable by the customer
|
||||
// rather than taken on faith. Amber ONLY on deviation — a green tier says nothing at all beyond
|
||||
// its timestamp, so the page stays quiet when everything is fine.
|
||||
if s.backupMgr != nil {
|
||||
if cd := s.backupMgr.SharesTier2Status(); cd != nil {
|
||||
data["SharesTier2Status"] = cd.LastStatus
|
||||
data["SharesTier2LastRun"] = cd.LastRun
|
||||
data["SharesTier2Warning"] = cd.LastWarning
|
||||
data["SharesTier2Error"] = cd.LastError
|
||||
data["SharesTier2Dest"] = cd.DestinationPath
|
||||
}
|
||||
if lastRun, status, count, ok := s.backupMgr.SharesOffsiteStatus(); ok {
|
||||
data["SharesOffsiteStatus"] = status
|
||||
data["SharesOffsiteLastRun"] = lastRun
|
||||
data["SharesOffsiteCount"] = count
|
||||
}
|
||||
data["SharesRestoreReady"] = s.backupMgr.SharesScratchReady()
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
|
||||
@@ -102,6 +102,25 @@
|
||||
{{else}}
|
||||
<p class="form-hint">Nincs távoli mentésre jelölt alkalmazás — a kijelölés a <a href="/backups/remote">Távoli mentés</a> oldalon történik.</p>
|
||||
{{end}}
|
||||
|
||||
<!-- R-7b: the shares source. Not an app row — it has no per-app toggle and no recovery unit —
|
||||
so it is its own entry. The reserved `_shares` key never appears here; the label always
|
||||
comes from the display mapping. -->
|
||||
{{if .SharesRestoreOffered}}
|
||||
<div class="app-row-list" style="margin-top:1rem">
|
||||
{{template "app_list_row" dict "Slug" "" "Name" .SharesDisplayName}}
|
||||
<form method="POST" action="/backup/shares/restore" style="display:inline">{{$.CSRFField}}
|
||||
<button type="submit" class="btn btn-xs btn-outline">Megosztások visszaállítása</button>
|
||||
</form>
|
||||
{{if .SharesScratchReady}}
|
||||
<form method="POST" action="/backup/shares/place" style="display:inline">{{$.CSRFField}}
|
||||
<button type="submit" class="btn btn-xs btn-outline">Helyreállítás az élő adatok közé (csak a hiányzó fájlok)</button>
|
||||
</form>
|
||||
<span class="form-hint" style="display:block;margin-top:.25rem">A meglévő fájlokat nem írja felül. A már létező megosztás-beállítások változatlanok maradnak.</span>
|
||||
{{end}}
|
||||
{{template "app_list_row_end"}}
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
||||
@@ -127,6 +127,51 @@
|
||||
A megosztás törlésekor <strong>a mappa és a fájlok megmaradnak</strong> — csak a hálózati
|
||||
elérés szűnik meg.
|
||||
</div>
|
||||
|
||||
<div class="backup-section-card" style="margin-top:1rem">
|
||||
<h3>A megosztások mentése</h3>
|
||||
<p class="form-hint" style="margin:-0.25rem 0 0.75rem">
|
||||
A megosztott mappák a többi adattal együtt mentésre kerülnek. A „Felhőmentés” bekapcsolva
|
||||
azt jelenti, hogy a mappa a távoli tárhelyre is felkerül; kikapcsolva csak a második
|
||||
meghajtóra készül másolat.
|
||||
</p>
|
||||
<ul class="plain-list">
|
||||
<li>
|
||||
<strong>2. mentés (másik meghajtó):</strong>
|
||||
{{if .SharesTier2Status}}
|
||||
{{if eq .SharesTier2Status "ok"}}
|
||||
rendben{{if .SharesTier2LastRun}} — {{.SharesTier2LastRun}}{{end}}
|
||||
{{if .SharesTier2Warning}}<span class="badge badge-warn">{{.SharesTier2Warning}}</span>{{end}}
|
||||
{{else if eq .SharesTier2Status "no_target"}}
|
||||
<span class="badge badge-warn">nincs cél — {{.SharesTier2Error}}</span>
|
||||
{{else}}
|
||||
<span class="badge badge-warn">hiba — {{.SharesTier2Error}}</span>
|
||||
{{end}}
|
||||
{{else}}
|
||||
még nem futott
|
||||
{{end}}
|
||||
</li>
|
||||
<li>
|
||||
<strong>Távoli mentés (felhő):</strong>
|
||||
{{if .SharesOffsiteStatus}}
|
||||
{{if eq .SharesOffsiteStatus "ok"}}
|
||||
rendben — {{.SharesOffsiteCount}} megosztás{{if .SharesOffsiteLastRun}}, {{.SharesOffsiteLastRun}}{{end}}
|
||||
{{else if eq .SharesOffsiteStatus "blocked"}}
|
||||
<span class="badge badge-warn">a tárhelykeret miatt csak a beállítások kerültek fel</span>
|
||||
{{else if eq .SharesOffsiteStatus "skipped"}}
|
||||
<span class="badge badge-warn">kimaradt</span>
|
||||
{{else}}
|
||||
<span class="badge badge-warn">hiba</span>
|
||||
{{end}}
|
||||
{{else}}
|
||||
még nem futott
|
||||
{{end}}
|
||||
</li>
|
||||
</ul>
|
||||
<div class="form-hint">
|
||||
Visszaállítani a <a href="/backups/restore">Visszaállítás</a> oldalon lehet.
|
||||
</div>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="backup-table-empty">Még nincs megosztott mappa.</div>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user