v0.279.0: after_install (decision 45), known default logins on the page, Part D empty-backup alarm, night chain (R-705), R-706
gates / gates (push) Successful in 27s
gates / gates (push) Successful in 27s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -75,6 +75,9 @@ func (s *Server) handleDebugAPI(w http.ResponseWriter, r *http.Request) {
|
||||
// it. Only the debug route was absent, so this is IMPLEMENTED rather than deleted.
|
||||
case subpath == "backup/crossdrive" && r.Method == http.MethodPost:
|
||||
s.debugRunCrossDrive(w, r)
|
||||
// R-705 (v0.279.0): the night's four legs, in order, now.
|
||||
case subpath == "backup/night-chain" && r.Method == http.MethodPost:
|
||||
s.debugRunNightChain(w, r)
|
||||
// R-397 — the button at debug.html:83 has posted here since it was added and NOTHING answered.
|
||||
// Verified 2026-08-30: this dispatch had no such case, so pressing „Restic integritás" did
|
||||
// nothing at all. Seventh instance of built-but-never-wired in this project; filed as R-400 in its
|
||||
|
||||
@@ -460,6 +460,10 @@ func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name stri
|
||||
data["Meta"] = meta
|
||||
data["AppConfig"] = appCfg
|
||||
data["AlreadyDeployed"] = alreadyDeployed
|
||||
// v0.279.0 (decision 45): the default login, before the install when nothing will replace it, after it
|
||||
// while it is still in effect.
|
||||
data["KnownLoginLine"] = s.knownLoginLine(lang, meta, appCfg, alreadyDeployed)
|
||||
data["DefaultLoginReplaced"] = meta.AfterInstall != nil && !defaultLoginInEffect(meta, appCfg, alreadyDeployed)
|
||||
data["LogoURL"] = s.cfg.AppLogoURL(meta.Slug)
|
||||
data["LogoPNGURL"] = s.cfg.AppLogoPNGURL(meta.Slug)
|
||||
data["AppPageURL"] = s.cfg.AppPageURL(meta.Slug)
|
||||
@@ -756,6 +760,9 @@ func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug s
|
||||
data["Stack"] = found
|
||||
data["Meta"] = found.Meta
|
||||
data["AppInfo"] = found.Meta.AppInfo
|
||||
// v0.279.0 (decision 45): the default-login card only while that login is in effect.
|
||||
data["KnownLoginLine"] = s.knownLoginLine(s.langFor(r), &found.Meta, found.AppConfig, found.Deployed)
|
||||
data["DefaultLoginReplaced"] = found.Meta.AfterInstall != nil && !defaultLoginInEffect(&found.Meta, found.AppConfig, found.Deployed)
|
||||
data["HasAppInfo"] = found.Meta.HasAppInfo()
|
||||
data["EffectiveSubdomain"] = effectiveSubdomain
|
||||
|
||||
@@ -1124,6 +1131,7 @@ func (s *Server) backupsAppsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data := s.backupsCommonData("backups-apps", "Biztonsági mentés — Alkalmazások", r)
|
||||
data["TitleKey"] = "page.title.backups_apps" // i18n: the Hungarian title above is what hu renders
|
||||
s.backupsOffboxData(data, s.langFor(r)) // the tier-3 rows render $.Offbox status
|
||||
data["HollowCopyLines"] = s.hollowCopyLines(s.langFor(r)) // Part D: running apps whose copy holds no data
|
||||
|
||||
if fullStatus, ok := data["Backup"].(*backup.FullBackupStatus); ok && fullStatus != nil {
|
||||
// Enrich AppDataInfo with storage labels
|
||||
@@ -1151,6 +1159,26 @@ func (s *Server) backupsAppsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
s.executeTemplate(w, r, "backups_apps", data)
|
||||
}
|
||||
|
||||
// hollowCopyLines (Part D, v0.279.0) — one sentence per running app whose newest copy holds no data.
|
||||
func (s *Server) hollowCopyLines(lang string) []string {
|
||||
if s.backupMgr == nil {
|
||||
return nil
|
||||
}
|
||||
var out []string
|
||||
for _, h := range s.backupMgr.HollowCopies() {
|
||||
name := h.App
|
||||
if st, ok := s.stackMgr.GetStack(h.App); ok && st.Meta.DisplayName != "" {
|
||||
name = st.Meta.DisplayName
|
||||
}
|
||||
key := "backups_apps.hollow_local"
|
||||
if h.Tier == backup.HollowTierOffsite {
|
||||
key = "backups_apps.hollow_offsite"
|
||||
}
|
||||
out = append(out, s.msgLang(lang, key, name))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// backupsRestoreHandler renders the Visszaállítás page: the restore panel, the offbox
|
||||
// restore-to-verify list and the .fab export/import loop.
|
||||
func (s *Server) backupsRestoreHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
)
|
||||
|
||||
// ── Known default logins (v0.279.0, `09` §3 decision 45) ────────────────────────────────────────────
|
||||
//
|
||||
// "An app is never published with a login a stranger knows." Where the template's after_install replaces
|
||||
// the default with a generated password, the default is gone once that command succeeded — the page must
|
||||
// then NOT show it (it would send the household to a login that no longer works). Where it cannot (no
|
||||
// after_install) or the command failed, the page and the install dialog say plainly what the default is and
|
||||
// to change it at once. Pinned by internal/web/known_login_test.go.
|
||||
|
||||
// defaultLoginInEffect: the template has a default login and nothing has replaced it on this install.
|
||||
// installed=false is the install dialog, before the install: a declared after_install WILL replace it.
|
||||
func defaultLoginInEffect(meta *stacks.Metadata, cfg *stacks.AppConfig, installed bool) bool {
|
||||
if meta == nil || strings.TrimSpace(meta.AppInfo.DefaultCreds) == "" {
|
||||
return false
|
||||
}
|
||||
if meta.AfterInstall == nil {
|
||||
return true
|
||||
}
|
||||
if !installed {
|
||||
return false
|
||||
}
|
||||
// Installed with an after_install: in effect only when the command FAILED (absent = not run yet — the
|
||||
// deploy-done hook runs it within minutes; the page does not warn about a default it is replacing).
|
||||
return cfg != nil && cfg.AfterInstall != nil && !cfg.AfterInstall.OK
|
||||
}
|
||||
|
||||
// knownLoginLine is the sentence, in lang, or "" when no default login is in effect.
|
||||
func (s *Server) knownLoginLine(lang string, meta *stacks.Metadata, cfg *stacks.AppConfig, installed bool) string {
|
||||
if !defaultLoginInEffect(meta, cfg, installed) {
|
||||
return ""
|
||||
}
|
||||
creds := strings.ReplaceAll(meta.AppInfo.DefaultCreds, "DOMAIN", s.cfg.Customer.Domain)
|
||||
return s.msgLang(lang, "app_info.known_login", creds)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
|
||||
)
|
||||
|
||||
// v0.279.0 (decision 45) — when the default login is IN EFFECT, and the sentence the household reads.
|
||||
// COMPANION RED-PROOF: make defaultLoginInEffect ignore after_install (the pre-0.279.0 page: the default
|
||||
// card always shown) → the "replaced" rows fail — the page would send the household to a dead login.
|
||||
func TestKnownLogin_InEffectOnlyUntilReplaced(t *testing.T) {
|
||||
withCreds := &stacks.Metadata{AppInfo: stacks.AppInfo{DefaultCreds: "admin / admin"}}
|
||||
withFix := &stacks.Metadata{AppInfo: stacks.AppInfo{DefaultCreds: "admin@claper.co / claper"},
|
||||
AfterInstall: &stacks.AfterInstallCommand{Service: "claper", Command: []string{"x"}, Success: "OK"}}
|
||||
ok := &stacks.AppConfig{AfterInstall: &stacks.AfterInstallRecord{OK: true}}
|
||||
failed := &stacks.AppConfig{AfterInstall: &stacks.AfterInstallRecord{OK: false}}
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
meta *stacks.Metadata
|
||||
cfg *stacks.AppConfig
|
||||
installed bool
|
||||
want bool
|
||||
}{
|
||||
{"no default login", &stacks.Metadata{}, nil, false, false},
|
||||
{"default, nothing replaces it: before install", withCreds, nil, false, true},
|
||||
{"default, nothing replaces it: installed", withCreds, &stacks.AppConfig{}, true, true},
|
||||
{"after_install declared: before install", withFix, nil, false, false},
|
||||
{"after_install succeeded", withFix, ok, true, false},
|
||||
{"after_install not run yet", withFix, &stacks.AppConfig{}, true, false},
|
||||
{"after_install FAILED", withFix, failed, true, true},
|
||||
} {
|
||||
if got := defaultLoginInEffect(c.meta, c.cfg, c.installed); got != c.want {
|
||||
t.Errorf("%s: in effect = %v, want %v", c.name, got, c.want)
|
||||
}
|
||||
}
|
||||
s := testServer(t)
|
||||
if got, want := s.knownLoginLine("hu", withCreds, nil, false), "Ez az alkalmazás egy ismert, közös jelszóval indul: admin / admin. Telepítés után azonnal változtasd meg."; got != want {
|
||||
t.Fatalf("hu sentence %q, want %q", got, want)
|
||||
}
|
||||
if got, want := s.knownLoginLine("en", withFix, failed, true), "This app starts with a known, shared password: admin@claper.co / claper. Change it right after the install."; got != want {
|
||||
t.Fatalf("en sentence %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ── R-705 (v0.279.0): "run tonight's chain now" — a debug action ─────────────────────────────────────
|
||||
//
|
||||
// The night runs four legs in ONE order (07 §6.1, 09 §6.4.2): the database/volume dump at W, the
|
||||
// second-drive copy at W+60m, the off-site copy at W+105m, and the automatic update leg chained after the
|
||||
// off-site one. Until now the only way to see that chain by day was to move the backup window and wait
|
||||
// two hours. This runs the same four legs, in the same order, one at a time, NOW. The whole-guest backup
|
||||
// (the agent's) is not part of it — it has no controller trigger (R-705 keeps that half open).
|
||||
//
|
||||
// Refused while any backup/restore op or guarded update runs, and while a chain is already running.
|
||||
|
||||
// nightChain is the four legs; each field is a seam so the order and the refusal are testable without
|
||||
// Docker or restic. production: newNightChain(s).
|
||||
type nightChain struct {
|
||||
dump func(ctx context.Context) error
|
||||
tier2 func()
|
||||
offsite func(ctx context.Context) error // nil: no off-site target on this box
|
||||
leg func(ctx context.Context)
|
||||
busy func() (bool, string)
|
||||
logf func(format string, args ...interface{})
|
||||
}
|
||||
|
||||
var nightChainRunning atomic.Bool
|
||||
|
||||
func (s *Server) newNightChain() nightChain {
|
||||
c := nightChain{
|
||||
dump: s.backupMgr.RunDBDumps,
|
||||
tier2: s.backupMgr.RunAllTier2,
|
||||
leg: func(ctx context.Context) { s.stackMgr.RunUpdateLegNow(ctx, "manual-chain") },
|
||||
logf: s.logger.Printf,
|
||||
busy: func() (bool, string) {
|
||||
if s.backupMgr.IsRunning() || s.backupMgr.RestoreStatus().Running {
|
||||
return true, "a backup or restore is running"
|
||||
}
|
||||
if s.stackMgr.AnyUpdating() {
|
||||
return true, "a guarded update is running"
|
||||
}
|
||||
return false, ""
|
||||
},
|
||||
}
|
||||
if s.backupMgr.OffboxRunnable() {
|
||||
c.offsite = s.backupMgr.RunOffboxBackup
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// start refuses or launches; it reports which legs will run.
|
||||
func (c nightChain) start() (bool, string, []string) {
|
||||
if busy, why := c.busy(); busy {
|
||||
return false, why, nil
|
||||
}
|
||||
if !nightChainRunning.CompareAndSwap(false, true) {
|
||||
return false, "the night's chain is already running", nil
|
||||
}
|
||||
legs := []string{"db-dump", "tier2", "offsite", "update-leg"}
|
||||
if c.offsite == nil {
|
||||
legs = []string{"db-dump", "tier2", "update-leg"}
|
||||
}
|
||||
go c.run()
|
||||
return true, "", legs
|
||||
}
|
||||
|
||||
func (c nightChain) run() {
|
||||
defer nightChainRunning.Store(false)
|
||||
ctx := context.Background()
|
||||
t0 := time.Now()
|
||||
step := func(name string, fn func() error) {
|
||||
s := time.Now()
|
||||
c.logf("[INFO] [night-chain] %s: started", name)
|
||||
if err := fn(); err != nil {
|
||||
c.logf("[WARN] [night-chain] %s: ended with an error after %s: %v — the chain goes on, as the night does", name, time.Since(s).Round(time.Second), err)
|
||||
return
|
||||
}
|
||||
c.logf("[INFO] [night-chain] %s: done in %s", name, time.Since(s).Round(time.Second))
|
||||
}
|
||||
c.logf("[INFO] [night-chain] manual run of tonight's chain: dump → second drive → off-site → update leg")
|
||||
step("db-dump", func() error { return c.dump(ctx) })
|
||||
step("tier2", func() error { c.tier2(); return nil })
|
||||
if c.offsite != nil {
|
||||
step("offsite", func() error { return c.offsite(ctx) })
|
||||
} else {
|
||||
c.logf("[INFO] [night-chain] offsite: no off-site target on this box — skipped, as at night")
|
||||
}
|
||||
step("update-leg", func() error { c.leg(ctx); return nil })
|
||||
c.logf("[INFO] [night-chain] finished in %s", time.Since(t0).Round(time.Second))
|
||||
}
|
||||
|
||||
func (s *Server) debugRunNightChain(w http.ResponseWriter, r *http.Request) {
|
||||
if s.backupMgr == nil || s.stackMgr == nil {
|
||||
writeDebugJSON(w, http.StatusBadRequest, false, "Backup manager nincs konfigurálva", nil)
|
||||
return
|
||||
}
|
||||
s.backupMgr.MarkManualRun()
|
||||
ok, why, legs := s.newNightChain().start()
|
||||
if !ok {
|
||||
s.logger.Printf("[WARN] [night-chain] manual run REFUSED: %s", why)
|
||||
writeDebugJSON(w, http.StatusConflict, false, "refused: "+why, nil)
|
||||
return
|
||||
}
|
||||
s.logger.Printf("[INFO] [night-chain] manual run started from %s: %v", r.RemoteAddr, legs)
|
||||
writeDebugJSON(w, http.StatusAccepted, true, "started", map[string]interface{}{"legs": legs})
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// R-705 (v0.279.0) — the manual night chain runs dump → Tier 2 → off-site → update leg, IN ORDER, one at a
|
||||
// time; it is refused while anything else runs and while a chain runs; with no off-site target it skips
|
||||
// only that leg. COMPANION RED-PROOF: swap two step calls in run() → the order assertion fails; drop the
|
||||
// busy() check in start() → the refusal assertion fails.
|
||||
func TestR705_NightChainRunsTheLegsInOrderAndRefusesWhenBusy(t *testing.T) {
|
||||
var mu sync.Mutex
|
||||
var order []string
|
||||
rec := func(n string) { mu.Lock(); order = append(order, n); mu.Unlock() }
|
||||
release := make(chan struct{})
|
||||
done := make(chan struct{})
|
||||
c := nightChain{
|
||||
dump: func(context.Context) error { rec("db-dump"); <-release; return nil },
|
||||
tier2: func() { rec("tier2") },
|
||||
offsite: func(context.Context) error { rec("offsite"); return nil },
|
||||
leg: func(context.Context) { rec("update-leg"); close(done) },
|
||||
busy: func() (bool, string) { return false, "" },
|
||||
logf: func(string, ...interface{}) {},
|
||||
}
|
||||
ok, why, legs := c.start()
|
||||
if !ok || !reflect.DeepEqual(legs, []string{"db-dump", "tier2", "offsite", "update-leg"}) {
|
||||
t.Fatalf("start: ok=%v why=%q legs=%v", ok, why, legs)
|
||||
}
|
||||
// A second press while the first chain runs is refused.
|
||||
if ok, why, _ := c.start(); ok || why != "the night's chain is already running" {
|
||||
t.Fatalf("second start while running: ok=%v why=%q", ok, why)
|
||||
}
|
||||
close(release)
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Fatal("the chain never reached the update leg")
|
||||
}
|
||||
for i := 0; i < 100 && nightChainRunning.Load(); i++ {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
if want := []string{"db-dump", "tier2", "offsite", "update-leg"}; !reflect.DeepEqual(order, want) {
|
||||
t.Fatalf("order %v, want %v", order, want)
|
||||
}
|
||||
|
||||
// Busy: refused, nothing runs.
|
||||
order = nil
|
||||
c.busy = func() (bool, string) { return true, "a guarded update is running" }
|
||||
if ok, why, _ := c.start(); ok || why != "a guarded update is running" {
|
||||
t.Fatalf("busy start: ok=%v why=%q", ok, why)
|
||||
}
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if len(order) != 0 {
|
||||
t.Fatalf("a refused chain ran legs: %v", order)
|
||||
}
|
||||
|
||||
// No off-site target: that leg alone is skipped.
|
||||
c.busy = func() (bool, string) { return false, "" }
|
||||
c.offsite = nil
|
||||
c.dump = func(context.Context) error { rec("db-dump"); return nil }
|
||||
done2 := make(chan struct{})
|
||||
c.leg = func(context.Context) { rec("update-leg"); close(done2) }
|
||||
if ok, _, legs := c.start(); !ok || !reflect.DeepEqual(legs, []string{"db-dump", "tier2", "update-leg"}) {
|
||||
t.Fatalf("no-offsite start: ok=%v legs=%v", ok, legs)
|
||||
}
|
||||
<-done2
|
||||
for i := 0; i < 100 && nightChainRunning.Load(); i++ {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
if want := []string{"db-dump", "tier2", "update-leg"}; !reflect.DeepEqual(order, want) {
|
||||
t.Fatalf("no-offsite order %v, want %v", order, want)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
||||
)
|
||||
|
||||
// Part D (v0.279.0) — the household sees the sentence on the REAL backup page (ServeHTTP → template), in
|
||||
// Hungarian by default, and the page is silent when nothing is flagged (negative control).
|
||||
// COMPANION RED-PROOF: drop `data["HollowCopyLines"]` from backupsAppsHandler → the sentence is absent.
|
||||
func TestPartD_TheBackupPageSaysARunningAppHasNoData(t *testing.T) {
|
||||
s := newDashboardServer(t, time.Time{})
|
||||
if s.backupMgr == nil {
|
||||
t.Fatal("fixture has no backup manager")
|
||||
}
|
||||
quiet := getPage(t, s, "/backups/apps")
|
||||
if quiet.Code != 200 || strings.Contains(quiet.Body.String(), "nem tartalmaz adatot") {
|
||||
t.Fatalf("with nothing flagged the page must be silent (code %d)", quiet.Code)
|
||||
}
|
||||
s.backupMgr.FlagHollowCopyForTest("cloudapp", backup.HollowTierOffsite)
|
||||
rec := getPage(t, s, "/backups/apps")
|
||||
body := rec.Body.String()
|
||||
if rec.Code != 200 || !strings.Contains(body, "A(z) cloudapp fut, de a legutóbbi távoli mentése nem tartalmaz adatot") {
|
||||
t.Fatalf("the flagged app's sentence is not on the page (code %d)", rec.Code)
|
||||
}
|
||||
}
|
||||
@@ -217,11 +217,11 @@ function appMigrate(btn,app,label){
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
{{if .AppInfo.DefaultCreds}}
|
||||
{{if and .AppInfo.DefaultCreds (not .DefaultLoginReplaced)}}
|
||||
<div class="app-info-card">
|
||||
<h3>{{T "app_info.alapertelmezett_belepes"}}</h3>
|
||||
<p class="app-info-creds">{{.AppInfo.DefaultCreds}}</p>
|
||||
<p class="app-info-creds-warn">{{T "app_info.az_elso_bejelentkezes_utan_azonnal_2"}}</p>
|
||||
{{if .KnownLoginLine}}<p class="app-info-creds-warn" id="known-login-line">{{.KnownLoginLine}}</p>{{else}}<p class="app-info-creds-warn">{{T "app_info.az_elso_bejelentkezes_utan_azonnal_2"}}</p>{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
|
||||
{{template "backups_flash" .}}
|
||||
{{template "restore_banner" .}}
|
||||
{{- if .HollowCopyLines}}
|
||||
<div class="alert alert-error" style="margin-bottom:1.5rem">
|
||||
{{range .HollowCopyLines}}<div>{{.}}</div>{{end}}
|
||||
</div>
|
||||
{{- end}}
|
||||
|
||||
{{if not .Backup}}
|
||||
{{template "backups_empty" .}}
|
||||
|
||||
@@ -79,6 +79,8 @@
|
||||
|
||||
<button class="btn btn-secondary btn-sm" id="btn-crossdrive" data-label="{{T "debug.csak_cross_drive"}}" onclick="triggerAction('btn-crossdrive','/api/debug/backup/crossdrive','POST')">{{T "debug.csak_cross_drive"}}</button>
|
||||
<span class="debug-result" id="btn-crossdrive-result"></span>
|
||||
<button class="btn btn-secondary btn-sm" id="btn-nightchain" data-label="{{T "debug.night_chain"}}" onclick="triggerAction('btn-nightchain','/api/debug/backup/night-chain','POST')">{{T "debug.night_chain"}}</button>
|
||||
<span class="debug-result" id="btn-nightchain-result"></span>
|
||||
|
||||
<button class="btn btn-secondary btn-sm" id="btn-integrity" data-label="{{T "debug.restic_integritas"}}" onclick="triggerAction('btn-integrity','/api/debug/backup/integrity','POST')">{{T "debug.restic_integritas"}}</button>
|
||||
<span class="debug-result" id="btn-integrity-result"></span>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
<div class="deploy-container">
|
||||
{{if .FlashSuccess}}<div class="flash flash-success">{{.FlashSuccess}}</div>{{end}}
|
||||
{{if .FlashError}}<div class="flash flash-error">{{.FlashError}}</div>{{end}}
|
||||
{{- if .KnownLoginLine}}<div class="flash flash-error" id="known-login-line">{{.KnownLoginLine}}</div>{{end}}
|
||||
<div class="deploy-info">
|
||||
<img class="deploy-logo" src="{{.LogoURL}}" alt="" data-fallback="/static/app-placeholder.svg"
|
||||
onerror="if(!this.dataset.step){this.dataset.step='1';this.src='{{.LogoPNGURL}}';}else if(this.dataset.fallback&&this.dataset.step==='1'){this.dataset.step='2';this.src=this.dataset.fallback;}else{this.onerror=null;this.style.visibility='hidden';}">
|
||||
@@ -674,7 +675,7 @@
|
||||
<script>
|
||||
var postDeployInfo = {
|
||||
firstSteps: {{json .Meta.AppInfo.FirstSteps}},
|
||||
defaultCreds: {{json .Meta.AppInfo.DefaultCreds}},
|
||||
defaultCreds: {{if .Meta.AfterInstall}}""{{else}}{{json .Meta.AppInfo.DefaultCreds}}{{end}},
|
||||
docsURL: {{json .Meta.AppInfo.DocsURL}},
|
||||
domain: {{json .Domain}},
|
||||
displayName: {{json .Meta.DisplayName}},
|
||||
|
||||
@@ -245,6 +245,8 @@
|
||||
|
||||
<button class="btn btn-secondary btn-sm" id="btn-crossdrive" data-label="Csak cross-drive" onclick="triggerAction('btn-crossdrive','/api/debug/backup/crossdrive','POST')">Csak cross-drive</button>
|
||||
<span class="debug-result" id="btn-crossdrive-result"></span>
|
||||
<button class="btn btn-secondary btn-sm" id="btn-nightchain" data-label="Az éjszakai lánc most (mentés → 2. meghajtó → távoli → frissítés)" onclick="triggerAction('btn-nightchain','/api/debug/backup/night-chain','POST')">Az éjszakai lánc most (mentés → 2. meghajtó → távoli → frissítés)</button>
|
||||
<span class="debug-result" id="btn-nightchain-result"></span>
|
||||
|
||||
<button class="btn btn-secondary btn-sm" id="btn-integrity" data-label="Restic integritás" onclick="triggerAction('btn-integrity','/api/debug/backup/integrity','POST')">Restic integritás</button>
|
||||
<span class="debug-result" id="btn-integrity-result"></span>
|
||||
|
||||
Reference in New Issue
Block a user