v0.243.0: FileBrowser generated admin password (R-513); per-tier whole-guest backup truth (R-517); skip absent-storage tiers (R-518); OOM-killed worker visible (R-514)
gates / gates (push) Successful in 14s
gates / gates (push) Successful in 14s
MinAgent: 0.131.0 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -113,6 +113,85 @@ type guestBackupView struct {
|
||||
RestoreTestedAt time.Time
|
||||
|
||||
CanTrigger bool // a backup trigger (quiesce loop) is wired
|
||||
|
||||
// Tiers (R-517, agent >= v0.131.0) is the per-tier truth. Empty on an older agent — the tile then
|
||||
// renders the single latest record as before.
|
||||
Tiers []guestTierView
|
||||
}
|
||||
|
||||
// guestTierView is one whole-guest tier as the customer sees it. A failed attempt is shown UNDER the
|
||||
// newest success, never instead of it ("presence is not success").
|
||||
type guestTierView struct {
|
||||
Target string
|
||||
Label string // Hungarian tier name
|
||||
IsPBS bool
|
||||
NotSetUp bool // the tier's storage does not exist on the host → „nincs beállítva"
|
||||
HasSuccess bool
|
||||
SuccessAt time.Time
|
||||
SizeKnown bool // false when the success was read back from storage after a restart
|
||||
SizeBytes int64
|
||||
Current bool // newest success is inside the tier's window
|
||||
FailedAfter bool // the last attempt failed and is newer than the newest success
|
||||
FailedAt time.Time
|
||||
}
|
||||
|
||||
// tierWindow is how old a tier's newest success may be and still count as current: its cadence plus
|
||||
// half again (a scheduled run lands inside the nightly window, not on the exact second). A tier with
|
||||
// no advertised cadence uses a day.
|
||||
func tierWindow(cadenceSecs int64) time.Duration {
|
||||
if cadenceSecs <= 0 {
|
||||
cadenceSecs = 24 * 3600
|
||||
}
|
||||
return time.Duration(cadenceSecs) * time.Second * 3 / 2
|
||||
}
|
||||
|
||||
// buildTierViews turns the agent's per-tier state into page rows and derives the three legacy tile
|
||||
// fields from SUCCESSES only: HasBackup/Success/Size/Target/StartedAt from the primary tier's newest
|
||||
// success; Due when any set-up tier has no current success; Offsite only on a current PBS success.
|
||||
func buildTierViews(v *guestBackupView, tiers []agentapi.TierBackupState, cadence map[string]int64, now time.Time) {
|
||||
v.Tiers = nil
|
||||
v.Due, v.Offsite, v.HasBackup = false, false, false
|
||||
var newestOK time.Time
|
||||
for _, t := range tiers {
|
||||
tv := guestTierView{Target: t.Target, IsPBS: strings.Contains(strings.ToLower(t.Target), "pbs")}
|
||||
if tv.IsPBS {
|
||||
tv.Label = "Biztonsági szerver – külön hardver (PBS)"
|
||||
} else {
|
||||
tv.Label = "Helyi tároló (" + t.Target + ")"
|
||||
}
|
||||
tv.NotSetUp = t.Storage == "absent"
|
||||
if t.LastSuccess != nil {
|
||||
if ts, err := time.Parse(time.RFC3339, t.LastSuccess.StartedAt); err == nil {
|
||||
tv.HasSuccess, tv.SuccessAt = true, ts
|
||||
tv.SizeKnown = t.LastSuccessSource != "storage"
|
||||
tv.SizeBytes = t.LastSuccess.SizeBytes
|
||||
tv.Current = now.Sub(ts) <= tierWindow(cadence[t.Target])
|
||||
}
|
||||
}
|
||||
if a := t.LastAttempt; a != nil && !a.Success {
|
||||
if ts, err := time.Parse(time.RFC3339, a.StartedAt); err == nil && (!tv.HasSuccess || ts.After(tv.SuccessAt)) {
|
||||
tv.FailedAfter, tv.FailedAt = true, ts
|
||||
}
|
||||
}
|
||||
if !tv.NotSetUp && !tv.Current {
|
||||
v.Due = true
|
||||
}
|
||||
if tv.IsPBS && tv.Current && !tv.NotSetUp {
|
||||
v.Offsite = true
|
||||
}
|
||||
if tv.HasSuccess && (t.Primary || !v.HasBackup) && (t.Primary || tv.SuccessAt.After(newestOK)) {
|
||||
v.HasBackup, v.Success = true, true
|
||||
v.StartedAt, v.SizeBytes, v.Target = tv.SuccessAt, tv.SizeBytes, tv.Label
|
||||
newestOK = tv.SuccessAt
|
||||
}
|
||||
v.Tiers = append(v.Tiers, tv)
|
||||
}
|
||||
if v.Due {
|
||||
v.DueReason = "tier_not_current"
|
||||
}
|
||||
if v.HasBackup {
|
||||
v.AgeHours = int64(now.Sub(v.StartedAt).Hours())
|
||||
}
|
||||
}
|
||||
|
||||
// loadGuestBackup fetches the agent's whole-guest backup view (best-effort). Returns a view with
|
||||
@@ -153,6 +232,17 @@ func (s *Server) loadGuestBackup(ctx context.Context) *guestBackupView {
|
||||
v.AgeHours = *due.AgeSecs / 3600
|
||||
}
|
||||
}
|
||||
// R-517: an agent that speaks per tier replaces the single-record fields with per-tier truth.
|
||||
// Done AFTER the legacy fill so an older agent keeps exactly the old rendering.
|
||||
if len(st.Tiers) > 0 {
|
||||
cadence := map[string]int64{}
|
||||
if tr, terr := client.BackupTiers(ctx); terr == nil {
|
||||
for _, t := range tr.Tiers {
|
||||
cadence[t.Target] = t.CadenceSeconds
|
||||
}
|
||||
}
|
||||
buildTierViews(v, st.Tiers, cadence, time.Now())
|
||||
}
|
||||
// Restore-test (the "verified restorable" trust signal; nil until one runs).
|
||||
if rt, rerr := client.RestoreTestStatus(ctx); rerr == nil && rt != nil {
|
||||
v.HasRestoreTest = true
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
|
||||
)
|
||||
|
||||
// R-517 — the whole-guest tile speaks from SUCCESSES per tier. The BIGNIGHT page read, after a local
|
||||
// success and a failed PBS attempt on absent storage: "✗ · 0 B · PBS · Naprakész" and ticked
|
||||
// "Távoli rendszermentés — külön hardveren".
|
||||
|
||||
var tvNow = time.Date(2026, 9, 14, 19, 15, 36, 0, time.UTC)
|
||||
|
||||
// RED-PROOF (run 2026-09-15, recorded in REPORT.md): with buildTierViews reading a tier's "success"
|
||||
// from LastAttempt instead of LastSuccess, the shown backup lost its size and this failed at "the
|
||||
// successful local backup is not the one shown: has=true size=0".
|
||||
func TestBuildTierViews_BignightFailedPBSOnAbsentStorage(t *testing.T) {
|
||||
v := &guestBackupView{}
|
||||
tiers := []agentapi.TierBackupState{
|
||||
{Target: "local", Primary: true, Storage: "present",
|
||||
LastSuccess: &agentapi.BackupRecord{TargetID: "local", Success: true, SizeBytes: 8877619753, StartedAt: "2026-09-14T19:03:23Z"},
|
||||
LastAttempt: &agentapi.TierAttempt{StartedAt: "2026-09-14T19:03:23Z", Success: true}},
|
||||
{Target: "felhom-pbs", Storage: "absent",
|
||||
LastAttempt: &agentapi.TierAttempt{StartedAt: "2026-09-14T19:09:59Z", Success: false, Error: "storage 'felhom-pbs' does not exist"}},
|
||||
}
|
||||
buildTierViews(v, tiers, map[string]int64{"local": 86400, "felhom-pbs": 604800}, tvNow)
|
||||
|
||||
if v.Offsite {
|
||||
t.Fatalf("remote tick shown without a PBS success: %+v", v.Tiers)
|
||||
}
|
||||
if !v.HasBackup || v.SizeBytes != 8877619753 || v.Target != "Helyi tároló (local)" {
|
||||
t.Fatalf("the successful local backup is not the one shown: has=%v size=%d target=%q", v.HasBackup, v.SizeBytes, v.Target)
|
||||
}
|
||||
if v.Due {
|
||||
t.Fatalf("a current local success with an unprovisioned PBS tier must read up to date, got Due")
|
||||
}
|
||||
pbs := v.Tiers[1]
|
||||
if !pbs.NotSetUp || pbs.HasSuccess || !pbs.FailedAfter {
|
||||
t.Fatalf("pbs row wrong (want not set up, no success, failed attempt shown): %+v", pbs)
|
||||
}
|
||||
}
|
||||
|
||||
// A set-up PBS tier whose last attempt failed: ✗ under the last success, and not current if the
|
||||
// success is outside the window → Due, no remote tick.
|
||||
func TestBuildTierViews_PBSFailedUnderOldSuccess(t *testing.T) {
|
||||
v := &guestBackupView{}
|
||||
tiers := []agentapi.TierBackupState{
|
||||
{Target: "local", Primary: true, Storage: "present",
|
||||
LastSuccess: &agentapi.BackupRecord{Success: true, SizeBytes: 10, StartedAt: "2026-09-14T01:00:00Z"}},
|
||||
{Target: "felhom-pbs", Storage: "present",
|
||||
LastSuccess: &agentapi.BackupRecord{Success: true, SizeBytes: 20, StartedAt: "2026-08-20T01:00:00Z"},
|
||||
LastAttempt: &agentapi.TierAttempt{StartedAt: "2026-09-14T02:00:00Z", Success: false}},
|
||||
}
|
||||
buildTierViews(v, tiers, map[string]int64{"local": 86400, "felhom-pbs": 604800}, tvNow)
|
||||
pbs := v.Tiers[1]
|
||||
if !pbs.HasSuccess || !pbs.FailedAfter || pbs.Current {
|
||||
t.Fatalf("pbs row: want old success kept, failure shown under it, not current: %+v", pbs)
|
||||
}
|
||||
if v.Offsite || !v.Due {
|
||||
t.Fatalf("want no remote tick and Due, got Offsite=%v Due=%v", v.Offsite, v.Due)
|
||||
}
|
||||
}
|
||||
|
||||
// After an agent restart the success is read back from storage: shown, size unknown.
|
||||
func TestBuildTierViews_SuccessFromStorageAfterRestart(t *testing.T) {
|
||||
v := &guestBackupView{}
|
||||
tiers := []agentapi.TierBackupState{
|
||||
{Target: "local", Primary: true, Storage: "present", LastSuccessSource: "storage",
|
||||
LastSuccess: &agentapi.BackupRecord{Success: true, StartedAt: "2026-09-14T19:03:23Z"}},
|
||||
}
|
||||
buildTierViews(v, tiers, map[string]int64{"local": 86400}, tvNow)
|
||||
if !v.HasBackup || v.Due || v.Tiers[0].SizeKnown {
|
||||
t.Fatalf("after restart: want the local success shown, up to date, size unknown: %+v / due=%v", v.Tiers[0], v.Due)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/crypto"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
)
|
||||
|
||||
// R-513 — the file manager's login is shown where app logins are, under the R-254 rule: the value is
|
||||
// never in the page, only behind the reveal act.
|
||||
|
||||
const fbTestPW = "TESTONLYfbPw7Kq2"
|
||||
|
||||
func renderFBPage(t *testing.T, operatorSet bool) string {
|
||||
t.Helper()
|
||||
s := securityHarness(t)
|
||||
s.loadTemplates()
|
||||
data := map[string]interface{}{
|
||||
"Page": "stacks", "Title": "FileBrowser", "Domain": "example.hu",
|
||||
"Stack": stacks.Stack{Name: "filebrowser", Deployed: true, State: "running"},
|
||||
"Meta": stacks.Metadata{DisplayName: "FileBrowser", Slug: "filebrowser"},
|
||||
"HasAppInfo": true,
|
||||
"InitialCreds": &stacks.ExtractedCreds{Available: true, Username: "admin"},
|
||||
}
|
||||
if operatorSet {
|
||||
data["InitialCredsOperatorSet"] = true
|
||||
} else {
|
||||
data["InitialCredsHasPassword"] = true
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := s.tmpl.ExecuteTemplate(&buf, "app_info", data); err != nil {
|
||||
t.Fatalf("render: %v", err)
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func TestFileBrowserLoginCard_Generated(t *testing.T) {
|
||||
html := renderFBPage(t, false)
|
||||
if !strings.Contains(html, "initial-credentials/reveal") || !strings.Contains(html, "admin") {
|
||||
t.Fatal("generated state: the login card has no reveal call or no username")
|
||||
}
|
||||
if strings.Contains(html, "zemeltet") { // „üzemeltető" — ASCII fragment
|
||||
t.Fatal("generated state shows the operator-set text")
|
||||
}
|
||||
}
|
||||
|
||||
// RED-PROOF (run 2026-09-15, recorded in REPORT.md): with the {{if .InitialCredsOperatorSet}} branch
|
||||
// removed from app_info.html, the operator page rendered the reveal button and this failed at
|
||||
// "operator state still offers a reveal".
|
||||
func TestFileBrowserLoginCard_OperatorSet(t *testing.T) {
|
||||
html := renderFBPage(t, true)
|
||||
if !strings.Contains(html, "zemeltet") {
|
||||
t.Fatal("operator state does not say the operator set the password")
|
||||
}
|
||||
if strings.Contains(html, `id="initcred-reveal"`) {
|
||||
t.Fatal("operator state still offers a reveal for a password the Felhom does not hold")
|
||||
}
|
||||
}
|
||||
|
||||
func revealFB(t *testing.T, s *Server) (int, map[string]any) {
|
||||
t.Helper()
|
||||
w := httptest.NewRecorder()
|
||||
s.fileBrowserPasswordReveal(w, httptest.NewRequest(http.MethodPost, "/apps/filebrowser/initial-credentials/reveal", nil))
|
||||
var body map[string]any
|
||||
_ = json.Unmarshal(w.Body.Bytes(), &body)
|
||||
return w.Code, body
|
||||
}
|
||||
|
||||
func TestFileBrowserPasswordReveal(t *testing.T) {
|
||||
s := securityHarness(t)
|
||||
s.encKey = bytes.Repeat([]byte{7}, 32)
|
||||
if code, _ := revealFB(t, s); code != http.StatusNotFound {
|
||||
t.Fatalf("undecided: want 404, got %d", code)
|
||||
}
|
||||
_ = s.settings.SetFileBrowserAdmin(settings.FileBrowserAdminOperator, "", "2026-09-15T00:00:00Z")
|
||||
if code, _ := revealFB(t, s); code != http.StatusNotFound {
|
||||
t.Fatalf("operator-set: want 404, got %d", code)
|
||||
}
|
||||
enc, err := crypto.Encrypt(s.encKey, fbTestPW)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = s.settings.SetFileBrowserAdmin(settings.FileBrowserAdminGenerated, enc, "2026-09-15T00:00:00Z")
|
||||
code, body := revealFB(t, s)
|
||||
data, _ := body["data"].(map[string]any)
|
||||
if code != http.StatusOK || data["password"] != fbTestPW {
|
||||
t.Fatalf("generated: want 200 + the decrypted password, got %d %v", code, body)
|
||||
}
|
||||
}
|
||||
@@ -180,6 +180,7 @@ func (s *Server) dashboardHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data["SettingsWarning"] = s.settings.LoadWarning // non-empty if settings.json was recovered from corruption
|
||||
data["Stacks"] = deployedStacks
|
||||
data["MissingStorage"] = s.missingStorageMap(deployedStacks)
|
||||
data["OOMKilled"] = s.stackMgr.OOMKilledStacks() // R-514
|
||||
nw, ns := s.networkStorageWarnings(deployedStacks) // NAS unreachable (recoverable) / guest-side stub (defect)
|
||||
data["NetworkWarnings"] = nw
|
||||
data["NetworkStubs"] = ns
|
||||
@@ -741,6 +742,24 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug s
|
||||
}
|
||||
}
|
||||
|
||||
// R-513 (v0.243.0): the file manager's login lives with the other app logins. The controller set a
|
||||
// generated password (state "generated") or found one set by hand ("operator"). Same R-254 rule:
|
||||
// only the username and a boolean reach the page; the value comes from the reveal endpoint.
|
||||
if found.Name == fileBrowserStack && s.settings != nil {
|
||||
state, enc, _ := s.settings.GetFileBrowserAdmin()
|
||||
switch state {
|
||||
case settings.FileBrowserAdminGenerated:
|
||||
data["HasAppInfo"] = true
|
||||
data["InitialCreds"] = &stacks.ExtractedCreds{Available: true, Username: "admin",
|
||||
Note: "A Fájlkezelő belépése. A jelszót a Felhom állította be ezen a gépen."}
|
||||
data["InitialCredsHasPassword"] = enc != ""
|
||||
case settings.FileBrowserAdminOperator:
|
||||
data["HasAppInfo"] = true
|
||||
data["InitialCreds"] = &stacks.ExtractedCreds{Available: true, Username: "admin"}
|
||||
data["InitialCredsOperatorSet"] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Per-app migration (B1): offer to move this app's data to another connected drive (≠ current).
|
||||
if found.Deployed {
|
||||
current := ""
|
||||
@@ -2261,6 +2280,11 @@ func (s *Server) appInitialCredsRevealHandler(w http.ResponseWriter, r *http.Req
|
||||
escrowJSON(w, http.StatusNotFound, nil, "Ismeretlen alkalmazás.")
|
||||
return
|
||||
}
|
||||
// R-513: the file manager's password is the controller's own stored value, not a container file.
|
||||
if found.Name == fileBrowserStack {
|
||||
s.fileBrowserPasswordReveal(w, r)
|
||||
return
|
||||
}
|
||||
creds, err := s.readInitialCreds(found.Name)
|
||||
if err != nil {
|
||||
// Never swallowed, and never surfaced raw — the error can name a container/path.
|
||||
@@ -2279,6 +2303,33 @@ func (s *Server) appInitialCredsRevealHandler(w http.ResponseWriter, r *http.Req
|
||||
escrowJSON(w, http.StatusOK, map[string]any{"password": creds.Password}, "")
|
||||
}
|
||||
|
||||
// fileBrowserStack is the protected infra stack whose admin password R-513 manages.
|
||||
const fileBrowserStack = "filebrowser"
|
||||
|
||||
// fileBrowserPasswordReveal serves the generated FileBrowser admin password (R-513) under the same
|
||||
// rules as every initial-credential reveal: POST + CSRF (router), no-store, logged as an act, and a
|
||||
// refusal that says why when there is nothing to show.
|
||||
func (s *Server) fileBrowserPasswordReveal(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
if s.settings == nil {
|
||||
escrowJSON(w, http.StatusServiceUnavailable, nil, "A beállítások nem elérhetők.")
|
||||
return
|
||||
}
|
||||
state, enc, _ := s.settings.GetFileBrowserAdmin()
|
||||
if state != settings.FileBrowserAdminGenerated || enc == "" {
|
||||
escrowJSON(w, http.StatusNotFound, nil, "A Fájlkezelő jelszavát nem a Felhom állította be ezen a gépen, ezért nem tudjuk megmutatni.")
|
||||
return
|
||||
}
|
||||
pw, err := crypto.Decrypt(s.encKey, enc)
|
||||
if err != nil || strings.TrimSpace(pw) == "" {
|
||||
s.logger.Printf("[ERROR] [web] filebrowser password reveal: decrypt failed: %v", err)
|
||||
escrowJSON(w, http.StatusInternalServerError, nil, "A jelszó most nem olvasható ki.")
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] filebrowser admin password revealed from %s (value never logged)", clientIP(r))
|
||||
escrowJSON(w, http.StatusOK, map[string]any{"password": pw}, "")
|
||||
}
|
||||
|
||||
func (s *Server) settingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
s.executeTemplate(w, r, "settings_system", s.systemPageData())
|
||||
}
|
||||
|
||||
@@ -187,6 +187,12 @@ function appMigrate(btn,app,label){
|
||||
<td><code class="initcred-user" style="user-select:all">{{.InitialCreds.Username}}</code></td>
|
||||
</tr>
|
||||
{{end}}
|
||||
{{if .InitialCredsOperatorSet}}
|
||||
<tr>
|
||||
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap">Jelszó</td>
|
||||
<td>az üzemeltető állította be</td>
|
||||
</tr>
|
||||
{{else}}
|
||||
<tr>
|
||||
<td class="initcred-label" style="padding:.25rem .75rem .25rem 0;color:var(--text-3);white-space:nowrap;vertical-align:middle">Jelszó</td>
|
||||
<!-- R-254: the value is NOT in this page. It used to be rendered into a `hidden`
|
||||
@@ -199,9 +205,10 @@ function appMigrate(btn,app,label){
|
||||
<span id="initcred-err" class="form-hint" style="display:none;color:var(--red)"></span>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
{{if .InitialCreds.Note}}<p class="app-info-creds-warn">{{.InitialCreds.Note}}</p>{{end}}
|
||||
<p class="app-info-creds-warn">Az első bejelentkezés után azonnal változtasd meg! Ez a kezdeti, automatikusan generált jelszó — ha már megváltoztattad, hagyd figyelmen kívül.</p>
|
||||
{{if not .InitialCredsOperatorSet}}<p class="app-info-creds-warn">Az első bejelentkezés után azonnal változtasd meg! Ez a kezdeti, automatikusan generált jelszó — ha már megváltoztattad, hagyd figyelmen kívül.</p>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
||||
@@ -109,14 +109,37 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{if .Tiers}}
|
||||
<table class="backup-tier-table" style="width:100%;margin-top:1rem;border-collapse:collapse">
|
||||
{{range .Tiers}}
|
||||
<tr style="border-top:1px solid var(--border)">
|
||||
<td style="padding:.5rem .75rem .5rem 0;white-space:nowrap;vertical-align:top"><strong>{{.Label}}</strong></td>
|
||||
<td style="padding:.5rem 0;vertical-align:top">
|
||||
{{if .NotSetUp}}
|
||||
<span class="tag"><span class="dot"></span>nincs beállítva</span>
|
||||
{{else}}
|
||||
{{if .HasSuccess}}
|
||||
<div>✓ Utolsó sikeres mentés: {{fmtTime .SuccessAt}} ({{timeAgo .SuccessAt}}){{if .SizeKnown}} · {{fmtBytes .SizeBytes}}{{end}}
|
||||
{{if .Current}}<span class="tag tag-run" style="margin-left:.4rem">Naprakész</span>{{else}}<span class="tag tag-warn" style="margin-left:.4rem">Esedékes</span>{{end}}</div>
|
||||
{{else}}
|
||||
<div>– Még nincs sikeres mentés <span class="tag tag-warn" style="margin-left:.4rem">Esedékes</span></div>
|
||||
{{end}}
|
||||
{{if .FailedAfter}}
|
||||
<div style="color:var(--red);margin-top:.25rem">✗ {{fmtTime .FailedAt}} — sikertelen</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</table>
|
||||
{{end}}
|
||||
{{if .Running}}
|
||||
<div class="alert alert-info" id="wg-running">Mentés folyamatban… (fázis: <span id="wg-phase">{{.Phase}}</span>)</div>
|
||||
{{end}}
|
||||
{{if .CanTrigger}}
|
||||
<div class="schedule-actions" style="margin-top:1rem">
|
||||
<button class="btn btn-sm btn-primary" id="wg-backup-btn" onclick="triggerGuestBackup()" {{if .Running}}disabled{{end}}>Mentés most</button>
|
||||
{{if .StopMode}}<span class="form-hint" style="margin-left:.5rem"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> A mentés idejére az alkalmazások rövid időre leállnak.</span>
|
||||
{{else}}<span class="form-hint" style="margin-left:.5rem">Pillanatkép-mód: az alkalmazások csak néhány másodpercre állnak le.</span>{{end}}
|
||||
<span class="form-hint" style="margin-left:.5rem"><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg> A mentés alatt az alkalmazások leállnak — általában néhány perc, nagyobb adatnál több.</span>
|
||||
<div id="wg-backup-result" style="margin-top:.6rem"></div>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -197,7 +220,7 @@
|
||||
// stop stacks → agent vzdump → resume). Returns immediately; we poll /api/guest-backup/status.
|
||||
function triggerGuestBackup() {
|
||||
var btn = document.getElementById('wg-backup-btn');
|
||||
felhomConfirm(btn, 'Elindítja a teljes rendszermentést most? A mentés ideje alatt az alkalmazások rövid időre leállhatnak.', function () {
|
||||
felhomConfirm(btn, 'Elindítod a teljes rendszermentést most? A mentés alatt az alkalmazások leállnak — általában néhány perc, nagyobb adatnál több.', function () {
|
||||
var out = document.getElementById('wg-backup-result');
|
||||
btn.disabled = true;
|
||||
out.innerHTML = '<span class="form-hint">Mentés indítása…</span>';
|
||||
|
||||
@@ -189,6 +189,7 @@
|
||||
{{if .Orphaned}}<span class="tag tag-warn">Elavult</span>{{end}}
|
||||
{{template "meta_badge" (lifecycleBadge .Meta)}}
|
||||
{{$ms := index $.MissingStorage .Name}}{{if $ms}}<span class="tag tag-warn" title="Az alkalmazás adattárolója nem elérhető. Csatlakoztasd újra a meghajtót, vagy helyezd át az adatokat egy másik tárhelyre."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>Hiányzó tárhely: {{$ms}}</span>{{end}}
|
||||
{{if $.OOMKilled}}{{if index $.OOMKilled .Name}}<span class="tag tag-warn" title="Az alkalmazás egyik folyamatát a rendszer leállította, mert elfogyott a memóriája. Lehet, hogy egy feladat nem fejeződött be. Indítsd újra az alkalmazást; ha ismétlődik, jelezd az üzemeltetőnek."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>Memória elfogyott</span>{{end}}{{end}}
|
||||
{{$ns := index $.NetworkStubs .Name}}{{if $ns}}<span class="tag tag-warn" title="Az alkalmazás környezetében a hálózati tárhely helyén üres helyi könyvtár van — az adatok nem a NAS-ra kerülnek. Jelezze az üzemeltetőnek."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>Hálózati tárhely hibás — az alkalmazás nem a NAS-t látja</span>{{end}}
|
||||
{{$nw := index $.NetworkWarnings .Name}}{{if $nw}}<span class="tag tag-warn" title="A hálózati tárhely (NAS) jelenleg nem érhető el. Az alkalmazás fut; az adatok elérése a NAS visszatértével helyreáll."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>Hálózati tárhely nem elérhető: {{$nw}}</span>{{end}}
|
||||
{{if and .Deployed (routeUnpublished .State)}}<span class="tag tag-warn" title="A proxy (Traefik) csak egészséges konténerhez publikál nyilvános útvonalat. Amíg az alkalmazás nem egészséges, az URL 404-et ad, pedig a konténer fut."><svg class="ico ico-sm"><use href="#i-triangle-alert"/></svg>URL nem elérhető</span>{{end}}
|
||||
|
||||
Reference in New Issue
Block a user