diff --git a/controller/internal/web/handlers.go b/controller/internal/web/handlers.go index f2c08f7..c7f79cc 100644 --- a/controller/internal/web/handlers.go +++ b/controller/internal/web/handlers.go @@ -1048,43 +1048,28 @@ func (s *Server) securityPageData() map[string]interface{} { return data } -// settingsData merges every subpage builder — legacy glue kept ONLY while the monolithic -// settings.html exists (deleted with the D1 Part 2 template split). -func (s *Server) settingsData() map[string]interface{} { - data := s.systemPageData() - for _, m := range []map[string]interface{}{s.storagePageData(), s.notificationsPageData(), s.securityPageData()} { - for k, v := range m { - if k == "Page" || k == "Title" { - continue - } - data[k] = v - } - } - return data -} - func (s *Server) settingsHandler(w http.ResponseWriter, r *http.Request) { - s.executeTemplate(w, r, "settings", s.settingsData()) + s.executeTemplate(w, r, "settings_system", s.systemPageData()) } // storagePageHandler serves the Tárhely main-nav page (D1). Storage action flashes land here. func (s *Server) storagePageHandler(w http.ResponseWriter, r *http.Request) { - data := s.settingsData() + data := s.storagePageData() if msg := r.URL.Query().Get("storage_msg"); msg == "success" { data["StorageSuccess"] = r.URL.Query().Get("storage_detail") } - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "storage", data) } // settingsNotificationsPageHandler serves GET /settings/notifications (the POST on the same // path is the save handler — dispatch is split in the router). func (s *Server) settingsNotificationsPageHandler(w http.ResponseWriter, r *http.Request) { - s.executeTemplate(w, r, "settings", s.settingsData()) + s.executeTemplate(w, r, "settings_notifications", s.notificationsPageData()) } // settingsSecurityPageHandler serves GET /settings/security. func (s *Server) settingsSecurityPageHandler(w http.ResponseWriter, r *http.Request) { - s.executeTemplate(w, r, "settings", s.settingsData()) + s.executeTemplate(w, r, "settings_security", s.securityPageData()) } func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request) { @@ -1097,7 +1082,7 @@ func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request) s.logger.Printf("[DEBUG] [web] settingsPasswordHandler: password change attempt from %s", r.RemoteAddr) } - data := s.settingsData() + data := s.securityPageData() // Validate current password effectiveHash := s.effectivePasswordHash() @@ -1106,21 +1091,21 @@ func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request) s.logger.Printf("[DEBUG] [web] settingsPasswordHandler: current password mismatch from %s", r.RemoteAddr) } data["PasswordError"] = "Hibás jelenlegi jelszó" - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "settings_security", data) return } // Validate new password length if len(newPassword) < 8 { data["PasswordError"] = "A jelszónak legalább 8 karakter hosszúnak kell lennie" - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "settings_security", data) return } // Validate passwords match if newPassword != confirmPassword { data["PasswordError"] = "A két jelszó nem egyezik" - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "settings_security", data) return } @@ -1129,7 +1114,7 @@ func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request) if err != nil { s.logger.Printf("[ERROR] [web] Failed to hash new password: %v", err) data["PasswordError"] = "Belső hiba a jelszó mentésekor" - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "settings_security", data) return } @@ -1137,7 +1122,7 @@ func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request) if err := s.settings.SetPasswordHash(string(hash)); err != nil { s.logger.Printf("[ERROR] [web] Failed to save password to settings.json: %v", err) data["PasswordError"] = "Belső hiba a jelszó mentésekor" - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "settings_security", data) return } @@ -1202,16 +1187,16 @@ func (s *Server) settingsNotificationsHandler(w http.ResponseWriter, r *http.Req if err := s.settings.SetNotificationPrefs(prefs); err != nil { s.logger.Printf("[ERROR] [web] Failed to save notification prefs: %v", err) - data := s.settingsData() + data := s.notificationsPageData() data["NotificationError"] = "Hiba a beállítások mentésekor" - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "settings_notifications", data) return } s.logger.Printf("[INFO] [web] Notification preferences updated: email=%s, events=%v", email, enabledEvents) // Sync preferences to hub - data := s.settingsData() + data := s.notificationsPageData() if s.notifier != nil && s.notifier.IsEnabled() { if err := s.notifier.SyncPreferences(email, enabledEvents, cooldownHours); err != nil { s.logger.Printf("[WARN] [web] Failed to sync preferences to hub: %v", err) @@ -1222,7 +1207,7 @@ func (s *Server) settingsNotificationsHandler(w http.ResponseWriter, r *http.Req } else { data["NotificationSuccess"] = "Értesítési beállítások mentve." } - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "settings_notifications", data) } // settingsAppEmailHandler saves the global app-email toggle and starts/stops the on-box @@ -1232,39 +1217,39 @@ func (s *Server) settingsAppEmailHandler(w http.ResponseWriter, r *http.Request) enabled := r.FormValue("app_email_enabled") == "on" || r.FormValue("app_email_enabled") == "true" fromName := strings.TrimSpace(r.FormValue("app_email_from_name")) - data := s.settingsData() + data := s.notificationsPageData() if err := s.settings.SetAppEmail(enabled, fromName); err != nil { s.logger.Printf("[ERROR] [web] Failed to save app-email toggle: %v", err) data["AppEmailError"] = "Hiba az alkalmazás-email beállítás mentésekor" - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "settings_notifications", data) return } // Reconcile the shim's running state with the new toggle. if s.mailShim != nil { if err := s.mailShim.Apply(enabled); err != nil { s.logger.Printf("[ERROR] [web] app-email shim could not be %s: %v", map[bool]string{true: "started", false: "stopped"}[enabled], err) - data = s.settingsData() + data = s.notificationsPageData() data["AppEmailError"] = "A beállítás elmentve, de az email-szolgáltatás indítása nem sikerült." - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "settings_notifications", data) return } } s.logger.Printf("[INFO] [web] App-email globally %s (from_name=%q)", map[bool]string{true: "enabled", false: "disabled"}[enabled], fromName) - data = s.settingsData() + data = s.notificationsPageData() if enabled { data["AppEmailSuccess"] = "Alkalmazás-email bekapcsolva. Kapcsold be az egyes alkalmazásoknál is, ahol email-küldést szeretnél." } else { data["AppEmailSuccess"] = "Alkalmazás-email kikapcsolva." } - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "settings_notifications", data) } func (s *Server) settingsNotificationsTestHandler(w http.ResponseWriter, r *http.Request) { - data := s.settingsData() + data := s.notificationsPageData() if s.notifier == nil { data["NotificationError"] = "Az értesítések nincsenek bekapcsolva" - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "settings_notifications", data) return } @@ -1272,12 +1257,12 @@ func (s *Server) settingsNotificationsTestHandler(w http.ResponseWriter, r *http if err != nil { s.logger.Printf("[ERROR] [web] Test notification failed: %v", err) data["NotificationError"] = fmt.Sprintf("Teszt email küldése sikertelen: %v", err) - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "settings_notifications", data) return } data["NotificationSuccess"] = "Teszt email elküldve." - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "settings_notifications", data) } // --- Storage path management handlers --- @@ -1496,27 +1481,27 @@ func (s *Server) settingsStorageAddHandler(w http.ResponseWriter, r *http.Reques label = settings.InferStorageLabel(path) } - data := s.settingsData() + data := s.storagePageData() // 1. Exists and is directory fi, err := os.Stat(path) if err != nil || !fi.IsDir() { data["StorageError"] = "Az útvonal nem létezik vagy nem mappa." - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "storage", data) return } // 2. Is mount point if !system.IsMountPoint(path) { data["StorageError"] = "Ez az útvonal nem külön csatlakoztatott meghajtó. Adatok az SSD-re kerülnének!" - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "storage", data) return } // 3. Writable if !system.IsWritable(path) { data["StorageError"] = "Az útvonal nem írható." - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "storage", data) return } @@ -1524,7 +1509,7 @@ func (s *Server) settingsStorageAddHandler(w http.ResponseWriter, r *http.Reques for _, existing := range s.settings.GetStoragePaths() { if system.PathsOverlap(path, existing.Path) { data["StorageError"] = fmt.Sprintf("Az útvonal átfedi a már regisztrált %s útvonalat.", existing.Path) - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "storage", data) return } } @@ -1545,7 +1530,7 @@ func (s *Server) settingsStorageAddHandler(w http.ResponseWriter, r *http.Reques if err := s.settings.AddStoragePath(sp); err != nil { s.logger.Printf("[ERROR] [web] Failed to add storage path: %v", err) data["StorageError"] = "Hiba a mentés során." - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "storage", data) return } @@ -1562,13 +1547,13 @@ func (s *Server) settingsStorageRemoveHandler(w http.ResponseWriter, r *http.Req s.logger.Printf("[DEBUG] [web] settingsStorageRemoveHandler: path=%s from %s", path, r.RemoteAddr) } - data := s.settingsData() + data := s.storagePageData() // Check: apps using this path apps := s.appsUsingPath(path) if len(apps) > 0 { data["StorageError"] = fmt.Sprintf("Nem törölhető: az alábbi alkalmazások használják: %s", strings.Join(apps, ", ")) - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "storage", data) return } @@ -1576,7 +1561,7 @@ func (s *Server) settingsStorageRemoveHandler(w http.ResponseWriter, r *http.Req for _, sp := range s.settings.GetStoragePaths() { if sp.Path == path && sp.IsDefault { data["StorageError"] = "Az alapértelmezett adattároló nem törölhető." - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "storage", data) return } } @@ -1584,13 +1569,13 @@ func (s *Server) settingsStorageRemoveHandler(w http.ResponseWriter, r *http.Req // Check: last path if len(s.settings.GetStoragePaths()) <= 1 { data["StorageError"] = "Az utolsó adattároló nem törölhető." - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "storage", data) return } if err := s.settings.RemoveStoragePath(path); err != nil { data["StorageError"] = "Hiba a törlés során." - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "storage", data) return } @@ -1645,17 +1630,17 @@ func (s *Server) settingsStorageLabelHandler(w http.ResponseWriter, r *http.Requ } if label == "" || len(label) > 50 { - data := s.settingsData() + data := s.storagePageData() data["StorageError"] = "A megnevezés nem lehet üres és legfeljebb 50 karakter." - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "storage", data) return } if err := s.settings.SetStorageLabel(path, label); err != nil { s.logger.Printf("[ERROR] [web] Failed to set storage label: %v", err) - data := s.settingsData() + data := s.storagePageData() data["StorageError"] = "Hiba a megnevezés mentésekor." - s.executeTemplate(w, r, "settings", data) + s.executeTemplate(w, r, "storage", data) return } diff --git a/controller/internal/web/settings_split_test.go b/controller/internal/web/settings_split_test.go index d3faded..78acda7 100644 --- a/controller/internal/web/settings_split_test.go +++ b/controller/internal/web/settings_split_test.go @@ -50,15 +50,80 @@ func getPage(t *testing.T, s *Server, path string) *httptest.ResponseRecorder { return rec } -// TestSettingsSplitPagesRender (D1 Scenario A skeleton): the four pages respond 200. -// Per-page unique-section markers are asserted once the template split lands (Part 2). +// TestSettingsSplitPagesRender (D1 Scenario A): each page responds 200, carries its own +// sections, and does NOT carry another page's sections (no cross-leak). func TestSettingsSplitPagesRender(t *testing.T) { s := testPageServer(t) - for _, path := range []string{"/settings", "/settings/notifications", "/settings/security", "/storage"} { - rec := getPage(t, s, path) + cases := []struct { + path string + must []string + mustNot []string + }{ + {"/settings", + []string{"Rendszer konfiguráció", "Verzió és frissítés", "Vezérlő újraindítása", "Kiszolgáló újraindítása"}, + []string{"Adattárolók", "Jelszó módosítás", "Értesítési szünet"}}, + {"/settings/notifications", + []string{"Beállítások — Értesítések"}, + []string{"Rendszer konfiguráció", "Adattárolók", "Jelszó módosítás"}}, + {"/settings/security", + []string{"Jelszó módosítás", "Földrajzi korlátozás"}, + []string{"Rendszer konfiguráció", "Adattárolók", "Értesítési szünet"}}, + {"/storage", + []string{"Adattárolók", "Hálózati tárhely (NAS)"}, + []string{"Rendszer konfiguráció", "Jelszó módosítás", "Értesítési szünet"}}, + } + for _, c := range cases { + rec := getPage(t, s, c.path) if rec.Code != 200 { - t.Errorf("GET %s = %d, want 200", path, rec.Code) + t.Errorf("GET %s = %d, want 200", c.path, rec.Code) + continue } + body := rec.Body.String() + for _, m := range c.must { + if !strings.Contains(body, m) { + t.Errorf("GET %s: missing section %q", c.path, m) + } + } + for _, m := range c.mustNot { + if strings.Contains(body, m) { + t.Errorf("GET %s: leaked foreign section %q", c.path, m) + } + } + } +} + +// TestSettingsSectionInventory (D1 §10): every h3 section of the pre-split settings.html is +// accounted for in the UNION of the four split templates (one deliberate rename noted). +func TestSettingsSectionInventory(t *testing.T) { + oldHeadings := []string{ + "Rendszer konfiguráció", + "Verzió és frissítés", + "Adattárolók", + "Hálózati tárhely (NAS)", + "Földrajzi korlátozás", + "Jelszó módosítás", + "Értesítések", + "Alkalmazás-email", + "Vészhelyzeti információk", // renamed from the misspelled "Veszhelyzeti informaciok" + "Vezérlő újraindítása", + "Kiszolgáló újraindítása", + } + var union strings.Builder + for _, f := range []string{"settings_system.html", "settings_notifications.html", "settings_security.html", "storage.html"} { + b, err := templateFS.ReadFile("templates/" + f) + if err != nil { + t.Fatalf("read %s: %v", f, err) + } + union.Write(b) + } + u := union.String() + for _, h := range oldHeadings { + if !strings.Contains(u, h) { + t.Errorf("old settings section %q missing from the union of the split templates", h) + } + } + if strings.Contains(u, "Veszhelyzeti informaciok") { + t.Error("the misspelled heading survived the split") } } diff --git a/controller/internal/web/templates/layout.html b/controller/internal/web/templates/layout.html index d19903b..e10aaad 100644 --- a/controller/internal/web/templates/layout.html +++ b/controller/internal/web/templates/layout.html @@ -23,12 +23,18 @@