5270bad76e
gates / gates (push) Successful in 23s
Slice 1 translated the dashboard's markup. The sentences the program BUILDS were still Hungarian literals in Go, so an English household clicked an English button and was answered in Hungarian. 226 of them move into the bundle here. A flash was the hard part: it travels inside the redirect URL and is rendered by a DIFFERENT request, so it now carries a bundle key plus its parameters. A link minted by an older controller carries prose and is shown verbatim — never a raw key, never dropped. Also converted: page data and view-model text, the internal/api JSON answers, the alert banners (Alert.MessageKey, rendered on the way out of GetAlerts), 237 country names at display, and the four page titles built around an app name (R-566 closed). Hungarian is byte-identical, and that is measured rather than read: scripts/i18n_go_parity.py freezes every Go literal at the base commit (7 467) and refuses a key whose Hungarian is not that text, byte for byte. Three decoys, each seen to convict. Its own first version filtered the capture through an ASCII-Hungarian word list and missed seven real literals — the R-565 class. The filter is gone. Nothing on the wire moved, and wire goldens now hold it there: the report's health warnings and every notify event message stay Hungarian, because the hub MAILS the controller's sentence when it has no entry of its own. Slice 3 (R-558) owns those. MinAgent: 0.131.0 (unchanged). No hub release needed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
136 lines
4.7 KiB
Go
136 lines
4.7 KiB
Go
package web
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
)
|
|
|
|
// Per-app Tier-2 (off-drive copy) config panel — item 4.
|
|
//
|
|
// The "2. mentés" row on the backup page used to link its "Beállítás" button at the app's deploy
|
|
// page, which has no backup-location setting (a dead end). This is the real surface: it shows the
|
|
// current/auto off-drive target + last-run status, and lets the customer pin a different registered
|
|
// drive or turn Tier 2 off. It is ALWAYS shown — even when only the internal SSD qualifies, or the
|
|
// app's data lives on the rootfs (already in PBS) — with honest context rather than a hidden control.
|
|
//
|
|
// Routes (wired in server.go, behind RequireAuth + CsrfProtect):
|
|
// GET /stacks/{name}/backup → tier2ConfigPageHandler
|
|
// POST /stacks/{name}/backup → tier2ConfigSaveHandler
|
|
|
|
func (s *Server) tier2ConfigPageHandler(w http.ResponseWriter, r *http.Request, name string) {
|
|
stack, ok := s.stackMgr.GetStack(name)
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if s.backupMgr == nil {
|
|
http.Error(w, "A mentés nincs beállítva ezen a szerveren.", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
|
|
info := s.backupMgr.Tier2Info(name)
|
|
|
|
data := s.baseData("backups", "2. mentés beállítása — "+stack.Meta.DisplayName)
|
|
data["StackName"] = name
|
|
data["DisplayName"] = stack.Meta.DisplayName
|
|
data["Tier2"] = info
|
|
if flash := s.flashFrom(r, "flash"); flash != "" {
|
|
data["Flash"] = flash
|
|
}
|
|
if flashErr := s.flashFrom(r, "flash_error"); flashErr != "" {
|
|
data["FlashError"] = flashErr
|
|
}
|
|
s.executeTemplate(w, r, "tier2_config", data)
|
|
}
|
|
|
|
func (s *Server) tier2ConfigSaveHandler(w http.ResponseWriter, r *http.Request, name string) {
|
|
if _, ok := s.stackMgr.GetStack(name); !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
if s.backupMgr == nil {
|
|
http.Error(w, "A mentés nincs beállítva ezen a szerveren.", http.StatusServiceUnavailable)
|
|
return
|
|
}
|
|
_ = r.ParseForm()
|
|
|
|
// "enabled" checkbox: present → Tier 2 on; absent → off (UserDisabled = !enabled).
|
|
enabled := r.FormValue("enabled") == "on" || r.FormValue("enabled") == "true"
|
|
target := r.FormValue("target") // "" = automatic; otherwise a registered drive path
|
|
|
|
// Validate the chosen target against the eligible alternatives (defence-in-depth: the runner
|
|
// also re-validates off-disk at run time, but reject a bogus path here for a clean message).
|
|
if target != "" {
|
|
valid := false
|
|
for _, opt := range s.backupMgr.Tier2Info(name).Alternatives {
|
|
if opt.Path == target {
|
|
valid = true
|
|
break
|
|
}
|
|
}
|
|
if !valid {
|
|
s.redirectTier2(w, r, name, "", "flash.tier2.target_invalid")
|
|
return
|
|
}
|
|
}
|
|
|
|
if err := s.settings.SetTier2Preference(name, !enabled, target); err != nil {
|
|
s.logger.Printf("[ERROR] [web] save Tier 2 preference for %s: %v", name, err)
|
|
s.redirectTier2(w, r, name, "", "flash.tier2.save_failed")
|
|
return
|
|
}
|
|
s.logger.Printf("[INFO] [web] Tier 2 preference saved for %s: enabled=%v target=%q", name, enabled, target)
|
|
|
|
// Apply immediately when enabled for an HDD app so the customer sees the result on return.
|
|
if enabled && s.backupMgr.Tier2Info(name).IsHDDApp {
|
|
go func() {
|
|
if err := s.backupMgr.RunTier2(name); err != nil {
|
|
s.logger.Printf("[WARN] [web] immediate Tier 2 run for %s failed: %v", name, err)
|
|
}
|
|
}()
|
|
}
|
|
|
|
s.redirectTier2(w, r, name, "flash.tier2.saved", "")
|
|
}
|
|
|
|
// appEmailToggleHandler flips the per-app email toggle (only for apps with an smtp_mapping)
|
|
// and recreates the stack so the SMTP env injection takes effect. Redirects back to the
|
|
// app's config page with a flash.
|
|
func (s *Server) appEmailToggleHandler(w http.ResponseWriter, r *http.Request, name string) {
|
|
if _, ok := s.stackMgr.GetStack(name); !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
_ = r.ParseForm()
|
|
enabled := r.FormValue("app_email_enabled") == "on" || r.FormValue("app_email_enabled") == "true"
|
|
|
|
dest := "/stacks/" + url.PathEscape(name) + "/deploy"
|
|
if err := s.stackMgr.SetAppEmailEnabled(name, enabled); err != nil {
|
|
s.logger.Printf("[ERROR] [web] app-email toggle for %s: %v", name, err)
|
|
http.Redirect(w, r, dest+"?flash_error="+url.QueryEscape(err.Error()), http.StatusSeeOther)
|
|
return
|
|
}
|
|
s.logger.Printf("[INFO] [web] App-email for %s set to %v", name, enabled)
|
|
msg := "flash.tier2.app_email_off"
|
|
if enabled {
|
|
msg = "flash.tier2.app_email_on"
|
|
}
|
|
http.Redirect(w, r, dest+"?"+flashQuery("flash", msg), http.StatusSeeOther)
|
|
}
|
|
|
|
// redirectTier2 sends the customer back to the panel with a flash message.
|
|
func (s *Server) redirectTier2(w http.ResponseWriter, r *http.Request, name, flash, flashErr string) {
|
|
dest := "/stacks/" + url.PathEscape(name) + "/backup"
|
|
q := url.Values{}
|
|
if flash != "" {
|
|
q.Set("flash", flash)
|
|
}
|
|
if flashErr != "" {
|
|
q.Set("flash_error", flashErr)
|
|
}
|
|
if e := q.Encode(); e != "" {
|
|
dest += "?" + e
|
|
}
|
|
http.Redirect(w, r, dest, http.StatusSeeOther)
|
|
}
|