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:
2026-10-12 15:38:28 +02:00
parent fd28d70bff
commit faef8f8a91
36 changed files with 421 additions and 1916 deletions
-36
View File
@@ -108,10 +108,6 @@ func (s *Server) handleDebugAPI(w http.ResponseWriter, r *http.Request) {
case subpath == "selfupdate/dry-run" && r.Method == http.MethodPost:
s.debugSelfUpdateDryRun(w, r)
// Section 7: DR / Setup
case subpath == "dr/trigger-setup" && r.Method == http.MethodPost:
s.debugTriggerSetupWizard(w, r)
// Section 8: Log viewer
case subpath == "logs" && r.Method == http.MethodGet:
s.debugLogBuffer(w, r)
@@ -686,38 +682,6 @@ func (s *Server) debugSelfUpdateDryRun(w http.ResponseWriter, r *http.Request) {
writeDebugJSON(w, http.StatusOK, true, "", result)
}
// ── Section 7: DR / Setup ───────────────────────────────────────────
func (s *Server) debugTriggerSetupWizard(w http.ResponseWriter, r *http.Request) {
var req struct {
Confirm string `json:"confirm"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeDebugJSON(w, http.StatusBadRequest, false, s.msg(r, "debug.api.invalid_request"), nil)
return
}
if req.Confirm != "RESET" {
writeDebugJSON(w, http.StatusBadRequest, false, s.msg(r, "debug.api.reset_confirm_invalid"), nil)
return
}
// Write marker file
markerPath := filepath.Join(s.cfg.Paths.DataDir, ".needs-setup")
if err := os.WriteFile(markerPath, []byte("debug-triggered\n"), 0644); err != nil {
writeDebugJSON(w, http.StatusInternalServerError, false,
s.msg(r, "debug.api.marker_write_failed", err), nil)
return
}
writeDebugJSON(w, http.StatusOK, true, s.msg(r, "debug.api.restarting_to_setup"), nil)
// Exit after response is sent so the container restarts into setup mode
go func() {
time.Sleep(500 * time.Millisecond)
os.Exit(0)
}()
}
// ── Section 8: Log viewer ───────────────────────────────────────────
func (s *Server) debugLogBuffer(w http.ResponseWriter, r *http.Request) {
@@ -51,6 +51,8 @@ func TestR400_DeletedControlsAreGoneFromTheTemplate(t *testing.T) {
{"/api/debug/storage/simulate-disconnect",
"no backing capability, and it WRITES storage state — a button that fakes a drive disconnect on a customer's machine is a foot-gun"},
{"/api/debug/storage/simulate-reconnect", "same as simulate-disconnect"},
{"/api/debug/dr/trigger-setup",
"R-554 (v0.308.0): it wrote .needs-setup and restarted the box into the first-boot setup wizard, which is deleted"},
}
for _, d := range deleted {
if strings.Contains(src, d.ref) {
@@ -60,7 +62,7 @@ func TestR400_DeletedControlsAreGoneFromTheTemplate(t *testing.T) {
// POSITIVE CONTROL. Without it this test passes against a template it failed to read, or one that
// was emptied — the exact shape of an assertion that proves nothing.
for _, kept := range []string{"/api/debug/backup/integrity", "/api/debug/dr/trigger-setup"} {
for _, kept := range []string{"/api/debug/backup/integrity", "/api/debug/hub/push"} {
if !strings.Contains(src, kept) {
t.Fatalf("positive control failed: %s is missing too, so the assertions above prove nothing "+
"about what was deliberately deleted", kept)
@@ -85,6 +87,9 @@ func TestR400_DeletedControlsLeftNoPanelOrScript(t *testing.T) {
"btn-infra-backup",
"btn-hub-infra",
"section-storage",
"btn-dr-trigger", // R-554: the setup-wizard trigger and its panel
"triggerDR",
"section-dr",
} {
if strings.Contains(src, orphan) {
t.Errorf("%q survived the deletion — an orphaned panel or handler renders nothing forever "+
@@ -92,7 +97,7 @@ func TestR400_DeletedControlsLeftNoPanelOrScript(t *testing.T) {
}
}
// POSITIVE CONTROL: the neighbours that must stay.
for _, kept := range []string{"btn-dr-trigger", "triggerDR", "section-dr", "btn-crossdrive"} {
for _, kept := range []string{"section-appexport", "btn-crossdrive"} {
if !strings.Contains(src, kept) {
t.Fatalf("positive control failed: %q is gone too — the deletion took a neighbour with it", kept)
}
@@ -163,8 +163,6 @@ func TestR516_WidenedFormalFormsAreGone(t *testing.T) {
t.Fatal(err)
}
want := map[string]string{
"debug.irja_be_reset": "Írd be: RESET",
"debug.api.reset_confirm_invalid": "Érvénytelen megerősítés — írd be: RESET",
"settings_security.adja_meg_a_jelenlegi_jelszavat": "Add meg a jelenlegi jelszavad",
"storage.biztosan_eltavolitja_adattarolot": "Biztosan eltávolítod a(z) {{.Path}} adattárolót?",
"storage_network.engedelyezze_a_felhom_gep_ip": "Engedélyezd a Felhom gép IP-címét a megosztáson. Proxmox-fürt esetén minden csomópont IP-címét.",
@@ -149,28 +149,6 @@
</div>
</div>
<!-- Section 7: DR / Setup Wizard -->
<div class="card debug-section" id="section-dr">
<div class="card-header debug-section-header" onclick="toggleSection('dr')">
<h3>{{T "debug.dr_telepito_varazslo"}}</h3>
<span class="section-toggle">▶</span>
</div>
<div class="card-body debug-section-body" style="display:none">
<div class="debug-actions" style="margin-top:1rem">
<div class="debug-dr-danger">
<p style="color:var(--crit);font-weight:600">{{T "debug.veszhelyzet_szimulacio"}}</p>
<p class="text-muted" style="font-size:.85rem;margin-bottom:.5rem">{{T "debug.ez_torli_az_ugyfel_azonositot"}}</p>
<div class="debug-form-row">
<label>{{T "debug.irja_be_reset_a_megerositeshez"}}</label>
<input type="text" id="dr-confirm-input" class="form-control debug-confirm-input" placeholder="RESET" autocomplete="off">
<button class="btn btn-danger btn-sm" id="btn-dr-trigger" data-label="{{T "debug.ujrainditas_setup_modban"}}" onclick="triggerDR()">{{T "debug.ujrainditas_setup_modban"}}</button>
<span class="debug-result" id="btn-dr-trigger-result"></span>
</div>
</div>
</div>
</div>
</div>
<!-- Section 9: App Export/Import -->
<div class="card debug-section" id="section-appexport">
<div class="card-header debug-section-header" onclick="toggleSection('appexport')">
@@ -452,43 +430,6 @@ function loadSelfUpdateStatus() {
});
}
// ── Section 7: DR ──
function triggerDR() {
var input = document.getElementById('dr-confirm-input');
if (input.value !== 'RESET') {
var result = document.getElementById('btn-dr-trigger-result');
result.className = 'debug-result debug-result-error';
result.textContent = '{{T "debug.irja_be_reset"}}';
return;
}
var btn = document.getElementById('btn-dr-trigger');
felhomConfirm(btn, '{{T "debug.figyelem_ez_ujrainditja_a_controllert"}}', function () {
var result = document.getElementById('btn-dr-trigger-result');
btn.disabled = true;
btn.textContent = '{{T "debug.folyamatban"}}';
fetch('/api/debug/dr/trigger-setup', {
method: 'POST',
headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()),
body: JSON.stringify({confirm: 'RESET'})
}).then(function(r){return r.json()}).then(function(data) {
if (data.ok) {
result.className = 'debug-result debug-result-ok';
result.textContent = '{{T "debug.controller_ujraindulasa"}}';
} else {
result.className = 'debug-result debug-result-error';
result.textContent = data.error || '{{T "debug.hiba"}}';
btn.disabled = false;
btn.textContent = btn.dataset.label;
}
}).catch(function(e) {
result.className = 'debug-result debug-result-error';
result.textContent = '{{T "debug.hiba_2"}} ' + e.message;
btn.disabled = false;
btn.textContent = btn.dataset.label;
});
});
}
// ── Section 8: Log viewer (Vezérlő | Ügynök tabs — v0.116.0) ──
var currentLogLevel = 'DEBUG';
var currentLogSource = 'controller';
-59
View File
@@ -321,28 +321,6 @@
</div>
<div class="card debug-section" id="section-dr">
<div class="card-header debug-section-header" onclick="toggleSection('dr')">
<h3>DR / Telepítő varázsló</h3>
<span class="section-toggle">▶</span>
</div>
<div class="card-body debug-section-body" style="display:none">
<div class="debug-actions" style="margin-top:1rem">
<div class="debug-dr-danger">
<p style="color:var(--crit);font-weight:600">Vészhelyzet szimuláció</p>
<p class="text-muted" style="font-size:.85rem;margin-bottom:.5rem">Ez törli az ügyfél azonosítót és újraindítja a controllert setup módba. A konfigurációs fájlok megmaradnak a meghajtókon.</p>
<div class="debug-form-row">
<label>Írd be "RESET" a megerősítéshez:</label>
<input type="text" id="dr-confirm-input" class="form-control debug-confirm-input" placeholder="RESET" autocomplete="off">
<button class="btn btn-danger btn-sm" id="btn-dr-trigger" data-label="Újraindítás setup módban" onclick="triggerDR()">Újraindítás setup módban</button>
<span class="debug-result" id="btn-dr-trigger-result"></span>
</div>
</div>
</div>
</div>
</div>
<div class="card debug-section" id="section-appexport">
<div class="card-header debug-section-header" onclick="toggleSection('appexport')">
<h3>Alkalmazás Export/Import</h3>
@@ -624,43 +602,6 @@ function loadSelfUpdateStatus() {
}
function triggerDR() {
var input = document.getElementById('dr-confirm-input');
if (input.value !== 'RESET') {
var result = document.getElementById('btn-dr-trigger-result');
result.className = 'debug-result debug-result-error';
result.textContent = 'Írd be: RESET';
return;
}
var btn = document.getElementById('btn-dr-trigger');
felhomConfirm(btn, 'FIGYELEM! Ez újraindítja a controllert setup módba. Biztosan folytatod?', function () {
var result = document.getElementById('btn-dr-trigger-result');
btn.disabled = true;
btn.textContent = 'Folyamatban...';
fetch('/api/debug/dr/trigger-setup', {
method: 'POST',
headers: Object.assign({'Content-Type':'application/json'}, csrfHeaders()),
body: JSON.stringify({confirm: 'RESET'})
}).then(function(r){return r.json()}).then(function(data) {
if (data.ok) {
result.className = 'debug-result debug-result-ok';
result.textContent = 'Controller újraindulása...';
} else {
result.className = 'debug-result debug-result-error';
result.textContent = data.error || 'Hiba';
btn.disabled = false;
btn.textContent = btn.dataset.label;
}
}).catch(function(e) {
result.className = 'debug-result debug-result-error';
result.textContent = 'Hiba: ' + e.message;
btn.disabled = false;
btn.textContent = btn.dataset.label;
});
});
}
var currentLogLevel = 'DEBUG';
var currentLogSource = 'controller';
var lastLogTimestamp = '';