v0.308.0 (R-554, held-back-able): recovery-info.txt points at the dashboard's Restore page and the recovery code; the first-boot setup wizard is deleted
Operator ruling 2026-10-12 (S27, 09 §3 decision 222). The operator approves the Hungarian text before release. A box whose first ingestion failed now serves one bilingual waiting page and retries every minute instead of the wizard. Own commit so the lead can hold it back. 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:
@@ -4,55 +4,64 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/i18n"
|
||||
)
|
||||
|
||||
// Info holds the data needed for the recovery info file and settings UI.
|
||||
// Info holds the data needed for the recovery info file.
|
||||
type Info struct {
|
||||
CustomerID string
|
||||
RetrievalPassword string
|
||||
HubURL string
|
||||
SupportEmail string
|
||||
SupportURL string
|
||||
// DashboardURL is the household's dashboard, e.g. https://felhom.<domain> (R-554, v0.308.0): the file
|
||||
// points at its Restore page and its „Adatok visszaszerzése" page, not at the deleted setup wizard.
|
||||
DashboardURL string
|
||||
// Lang is the box's language; the file is written in it (Hungarian when empty or unknown).
|
||||
Lang string
|
||||
}
|
||||
|
||||
// GenerateRecoveryFile writes a plain text recovery-info.txt to the given directory.
|
||||
func GenerateRecoveryFile(info Info, version, outputDir string) error {
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating recovery info directory: %w", err)
|
||||
// recoveryInfoKey is the file's text in the bundle (hu.json / en.json), with ten %s, in order:
|
||||
// customer id, hub URL, retrieval password, support e-mail, support web, dashboard, restore page,
|
||||
// recovery page, generated-at, controller version.
|
||||
const recoveryInfoKey = "recovery_info.body"
|
||||
|
||||
// Render returns the text of recovery-info.txt (R-554, `09` §3 decision 222): what to do when something must
|
||||
// be restored — the dashboard's Backup → Restore page, and after a reinstall the recovery code on the
|
||||
// „Adatok visszaszerzése" page. Pinned by TestRecoveryInfo_PointsAtDashboardNotWizard.
|
||||
func Render(info Info, version string, now time.Time) (string, error) {
|
||||
b, err := i18n.Shared()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("loading message bundle: %w", err)
|
||||
}
|
||||
|
||||
content := fmt.Sprintf(`Felhom Controller — Vészhelyzeti információk
|
||||
=============================================
|
||||
|
||||
Ügyfél azonosító: %s
|
||||
Hub URL: %s
|
||||
Visszaállítási jelszó: %s
|
||||
|
||||
Támogatás:
|
||||
Email: %s
|
||||
Web: %s
|
||||
|
||||
Visszaállítási útmutató:
|
||||
1. Telepítse az operációs rendszert (Debian 13)
|
||||
2. Futtassa a docker-setup.sh szkriptet:
|
||||
sudo ./docker-setup.sh --domain <domain>
|
||||
3. Nyissa meg a böngészőben: http://<ip>:8081
|
||||
4. Válassza a "Visszaállítás mentésből" opciót
|
||||
5. Adja meg az ügyfél azonosítót és a visszaállítási jelszót
|
||||
6. Kövesse a varázsló utasításait
|
||||
|
||||
Generálva: %s
|
||||
Controller verzió: %s
|
||||
`,
|
||||
dash := strings.TrimRight(info.DashboardURL, "/")
|
||||
return b.Msgf(i18n.Normalize(info.Lang), recoveryInfoKey,
|
||||
info.CustomerID,
|
||||
info.HubURL,
|
||||
info.RetrievalPassword,
|
||||
info.SupportEmail,
|
||||
info.SupportURL,
|
||||
time.Now().Format("2006-01-02 15:04:05"),
|
||||
dash,
|
||||
dash+"/backups/restore",
|
||||
dash+"/recovery",
|
||||
now.Format("2006-01-02 15:04:05"),
|
||||
version,
|
||||
)
|
||||
), nil
|
||||
}
|
||||
|
||||
// GenerateRecoveryFile writes a plain text recovery-info.txt to the given directory (0600: it holds the
|
||||
// retrieval password).
|
||||
func GenerateRecoveryFile(info Info, version, outputDir string) error {
|
||||
if err := os.MkdirAll(outputDir, 0755); err != nil {
|
||||
return fmt.Errorf("creating recovery info directory: %w", err)
|
||||
}
|
||||
content, err := Render(info, version, time.Now())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := filepath.Join(outputDir, "recovery-info.txt")
|
||||
tmp := path + ".tmp"
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package recovery
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// R-554 (`09` §3 decision 222): recovery-info.txt points at the dashboard's Restore page and the recovery
|
||||
// code — never at the deleted first-boot wizard (docker-setup.sh, :8081, „Visszaállítás mentésből").
|
||||
//
|
||||
// RED-PROOF (REPORT): the old text (docker-setup.sh … http://<ip>:8081 … varázsló) fails every
|
||||
// "must carry" line and every "must not carry" line.
|
||||
func TestRecoveryInfo_PointsAtDashboardNotWizard(t *testing.T) {
|
||||
info := Info{
|
||||
CustomerID: "c-123", RetrievalPassword: "TESTONLY-retrieval-pw", HubURL: "https://hub.example",
|
||||
SupportEmail: "support@felhom.eu", SupportURL: "https://felhom.eu/kapcsolat",
|
||||
DashboardURL: "https://felhom.example.hu/",
|
||||
}
|
||||
at := time.Date(2026, 10, 12, 15, 4, 5, 0, time.UTC)
|
||||
for _, tc := range []struct {
|
||||
lang string
|
||||
must []string
|
||||
}{
|
||||
{"hu", []string{"Biztonsági mentés → Visszaállítás", "helyreállítási kód", "Adatok visszaszerzése"}},
|
||||
{"en", []string{"Backup → Restore", "recovery code", "Retrieve data"}},
|
||||
} {
|
||||
info.Lang = tc.lang
|
||||
got, err := Render(info, "0.308.0", at)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, want := range append(tc.must,
|
||||
"https://felhom.example.hu/backups/restore", "https://felhom.example.hu/recovery",
|
||||
"c-123", "https://hub.example", "TESTONLY-retrieval-pw", "2026-10-12 15:04:05", "0.308.0") {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("%s: the file does not carry %q", tc.lang, want)
|
||||
}
|
||||
}
|
||||
for _, gone := range []string{"docker-setup.sh", ":8081", "Visszaállítás mentésből", "varázsló", "wizard", "%!"} {
|
||||
if strings.Contains(got, gone) {
|
||||
t.Errorf("%s: the file still carries %q", tc.lang, gone)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The file is written 0600 (it holds the retrieval password) and in the box's language.
|
||||
func TestGenerateRecoveryFile_WritesPrivateFile(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
if err := GenerateRecoveryFile(Info{CustomerID: "c-1", DashboardURL: "https://felhom.x.hu", Lang: "en"}, "0.308.0", dir); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p := filepath.Join(dir, "recovery-info.txt")
|
||||
fi, err := os.Stat(p)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fi.Mode().Perm() != 0o600 {
|
||||
t.Errorf("mode = %v, want 0600", fi.Mode().Perm())
|
||||
}
|
||||
b, _ := os.ReadFile(p)
|
||||
if !strings.Contains(string(b), "Emergency information") {
|
||||
t.Errorf("an English box's file is not in English")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user