D1 Part 2: settings.html split into four pages + sidebar restructure
- settings.html (1451 lines) deleted; sections moved verbatim into
settings_system.html (Rendszer konfiguráció, Verzió és frissítés,
Vezérlő/Kiszolgáló újraindítása + update/restart JS),
settings_notifications.html (Értesítések, Alkalmazás-email),
settings_security.html (Jelszó módosítás, Földrajzi korlátozás + geo
JS, Vészhelyzeti információk — heading + section copy accents fixed),
storage.html (Adattárolók, NAS, migrate progress, agent view + all
storage JS; wizard entry links now /storage/init|attach with sprite
icons instead of emoji). The NAS + migrate sections were nested inside
{{if .StoragePaths}} in the monolith and vanished with zero drives —
now unconditional on /storage.
- layout.html: Tárhely main-nav item (hard-drive icon) + the
'Beállítások' sidebar group with Rendszer / Értesítések / Biztonság és
hozzáférés sub-links (active-state per page key); orphaned
.sidebar-settings-link CSS deleted (grep-zero), .nav-group-label /
.nav-links-sub added.
- Handlers wired to their own builders + templates; the legacy
settingsData() merge deleted.
- scripts/template_id_gate.py: the §10 JS element-ID integrity gate
(getElementById/querySelector('#…') must resolve in the SAME template;
JS-created + template-parameterized IDs handled; layout modal IDs
allowlisted). Red-proven: a storage function planted in the
notifications template failed the gate with 'static #migrate-progress
not defined'.
- Tests: per-page section markers + cross-leak assertions, h3 section
inventory (all 11 old headings accounted for; typo rename asserted).
This commit is contained in:
@@ -1048,43 +1048,28 @@ func (s *Server) securityPageData() map[string]interface{} {
|
|||||||
return data
|
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) {
|
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.
|
// storagePageHandler serves the Tárhely main-nav page (D1). Storage action flashes land here.
|
||||||
func (s *Server) storagePageHandler(w http.ResponseWriter, r *http.Request) {
|
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" {
|
if msg := r.URL.Query().Get("storage_msg"); msg == "success" {
|
||||||
data["StorageSuccess"] = r.URL.Query().Get("storage_detail")
|
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
|
// settingsNotificationsPageHandler serves GET /settings/notifications (the POST on the same
|
||||||
// path is the save handler — dispatch is split in the router).
|
// path is the save handler — dispatch is split in the router).
|
||||||
func (s *Server) settingsNotificationsPageHandler(w http.ResponseWriter, r *http.Request) {
|
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.
|
// settingsSecurityPageHandler serves GET /settings/security.
|
||||||
func (s *Server) settingsSecurityPageHandler(w http.ResponseWriter, r *http.Request) {
|
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) {
|
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)
|
s.logger.Printf("[DEBUG] [web] settingsPasswordHandler: password change attempt from %s", r.RemoteAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := s.settingsData()
|
data := s.securityPageData()
|
||||||
|
|
||||||
// Validate current password
|
// Validate current password
|
||||||
effectiveHash := s.effectivePasswordHash()
|
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)
|
s.logger.Printf("[DEBUG] [web] settingsPasswordHandler: current password mismatch from %s", r.RemoteAddr)
|
||||||
}
|
}
|
||||||
data["PasswordError"] = "Hibás jelenlegi jelszó"
|
data["PasswordError"] = "Hibás jelenlegi jelszó"
|
||||||
s.executeTemplate(w, r, "settings", data)
|
s.executeTemplate(w, r, "settings_security", data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate new password length
|
// Validate new password length
|
||||||
if len(newPassword) < 8 {
|
if len(newPassword) < 8 {
|
||||||
data["PasswordError"] = "A jelszónak legalább 8 karakter hosszúnak kell lennie"
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate passwords match
|
// Validate passwords match
|
||||||
if newPassword != confirmPassword {
|
if newPassword != confirmPassword {
|
||||||
data["PasswordError"] = "A két jelszó nem egyezik"
|
data["PasswordError"] = "A két jelszó nem egyezik"
|
||||||
s.executeTemplate(w, r, "settings", data)
|
s.executeTemplate(w, r, "settings_security", data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1129,7 +1114,7 @@ func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Printf("[ERROR] [web] Failed to hash new password: %v", err)
|
s.logger.Printf("[ERROR] [web] Failed to hash new password: %v", err)
|
||||||
data["PasswordError"] = "Belső hiba a jelszó mentésekor"
|
data["PasswordError"] = "Belső hiba a jelszó mentésekor"
|
||||||
s.executeTemplate(w, r, "settings", data)
|
s.executeTemplate(w, r, "settings_security", data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1137,7 +1122,7 @@ func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request)
|
|||||||
if err := s.settings.SetPasswordHash(string(hash)); err != nil {
|
if err := s.settings.SetPasswordHash(string(hash)); err != nil {
|
||||||
s.logger.Printf("[ERROR] [web] Failed to save password to settings.json: %v", err)
|
s.logger.Printf("[ERROR] [web] Failed to save password to settings.json: %v", err)
|
||||||
data["PasswordError"] = "Belső hiba a jelszó mentésekor"
|
data["PasswordError"] = "Belső hiba a jelszó mentésekor"
|
||||||
s.executeTemplate(w, r, "settings", data)
|
s.executeTemplate(w, r, "settings_security", data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1202,16 +1187,16 @@ func (s *Server) settingsNotificationsHandler(w http.ResponseWriter, r *http.Req
|
|||||||
|
|
||||||
if err := s.settings.SetNotificationPrefs(prefs); err != nil {
|
if err := s.settings.SetNotificationPrefs(prefs); err != nil {
|
||||||
s.logger.Printf("[ERROR] [web] Failed to save notification prefs: %v", err)
|
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"
|
data["NotificationError"] = "Hiba a beállítások mentésekor"
|
||||||
s.executeTemplate(w, r, "settings", data)
|
s.executeTemplate(w, r, "settings_notifications", data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
s.logger.Printf("[INFO] [web] Notification preferences updated: email=%s, events=%v", email, enabledEvents)
|
s.logger.Printf("[INFO] [web] Notification preferences updated: email=%s, events=%v", email, enabledEvents)
|
||||||
|
|
||||||
// Sync preferences to hub
|
// Sync preferences to hub
|
||||||
data := s.settingsData()
|
data := s.notificationsPageData()
|
||||||
if s.notifier != nil && s.notifier.IsEnabled() {
|
if s.notifier != nil && s.notifier.IsEnabled() {
|
||||||
if err := s.notifier.SyncPreferences(email, enabledEvents, cooldownHours); err != nil {
|
if err := s.notifier.SyncPreferences(email, enabledEvents, cooldownHours); err != nil {
|
||||||
s.logger.Printf("[WARN] [web] Failed to sync preferences to hub: %v", err)
|
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 {
|
} else {
|
||||||
data["NotificationSuccess"] = "Értesítési beállítások mentve."
|
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
|
// 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"
|
enabled := r.FormValue("app_email_enabled") == "on" || r.FormValue("app_email_enabled") == "true"
|
||||||
fromName := strings.TrimSpace(r.FormValue("app_email_from_name"))
|
fromName := strings.TrimSpace(r.FormValue("app_email_from_name"))
|
||||||
|
|
||||||
data := s.settingsData()
|
data := s.notificationsPageData()
|
||||||
if err := s.settings.SetAppEmail(enabled, fromName); err != nil {
|
if err := s.settings.SetAppEmail(enabled, fromName); err != nil {
|
||||||
s.logger.Printf("[ERROR] [web] Failed to save app-email toggle: %v", err)
|
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"
|
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
|
return
|
||||||
}
|
}
|
||||||
// Reconcile the shim's running state with the new toggle.
|
// Reconcile the shim's running state with the new toggle.
|
||||||
if s.mailShim != nil {
|
if s.mailShim != nil {
|
||||||
if err := s.mailShim.Apply(enabled); err != 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)
|
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."
|
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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
s.logger.Printf("[INFO] [web] App-email globally %s (from_name=%q)", map[bool]string{true: "enabled", false: "disabled"}[enabled], fromName)
|
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 {
|
if enabled {
|
||||||
data["AppEmailSuccess"] = "Alkalmazás-email bekapcsolva. Kapcsold be az egyes alkalmazásoknál is, ahol email-küldést szeretnél."
|
data["AppEmailSuccess"] = "Alkalmazás-email bekapcsolva. Kapcsold be az egyes alkalmazásoknál is, ahol email-küldést szeretnél."
|
||||||
} else {
|
} else {
|
||||||
data["AppEmailSuccess"] = "Alkalmazás-email kikapcsolva."
|
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) {
|
func (s *Server) settingsNotificationsTestHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
data := s.settingsData()
|
data := s.notificationsPageData()
|
||||||
|
|
||||||
if s.notifier == nil {
|
if s.notifier == nil {
|
||||||
data["NotificationError"] = "Az értesítések nincsenek bekapcsolva"
|
data["NotificationError"] = "Az értesítések nincsenek bekapcsolva"
|
||||||
s.executeTemplate(w, r, "settings", data)
|
s.executeTemplate(w, r, "settings_notifications", data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1272,12 +1257,12 @@ func (s *Server) settingsNotificationsTestHandler(w http.ResponseWriter, r *http
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Printf("[ERROR] [web] Test notification failed: %v", err)
|
s.logger.Printf("[ERROR] [web] Test notification failed: %v", err)
|
||||||
data["NotificationError"] = fmt.Sprintf("Teszt email küldése sikertelen: %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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data["NotificationSuccess"] = "Teszt email elküldve."
|
data["NotificationSuccess"] = "Teszt email elküldve."
|
||||||
s.executeTemplate(w, r, "settings", data)
|
s.executeTemplate(w, r, "settings_notifications", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Storage path management handlers ---
|
// --- Storage path management handlers ---
|
||||||
@@ -1496,27 +1481,27 @@ func (s *Server) settingsStorageAddHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
label = settings.InferStorageLabel(path)
|
label = settings.InferStorageLabel(path)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := s.settingsData()
|
data := s.storagePageData()
|
||||||
|
|
||||||
// 1. Exists and is directory
|
// 1. Exists and is directory
|
||||||
fi, err := os.Stat(path)
|
fi, err := os.Stat(path)
|
||||||
if err != nil || !fi.IsDir() {
|
if err != nil || !fi.IsDir() {
|
||||||
data["StorageError"] = "Az útvonal nem létezik vagy nem mappa."
|
data["StorageError"] = "Az útvonal nem létezik vagy nem mappa."
|
||||||
s.executeTemplate(w, r, "settings", data)
|
s.executeTemplate(w, r, "storage", data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Is mount point
|
// 2. Is mount point
|
||||||
if !system.IsMountPoint(path) {
|
if !system.IsMountPoint(path) {
|
||||||
data["StorageError"] = "Ez az útvonal nem külön csatlakoztatott meghajtó. Adatok az SSD-re kerülnének!"
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Writable
|
// 3. Writable
|
||||||
if !system.IsWritable(path) {
|
if !system.IsWritable(path) {
|
||||||
data["StorageError"] = "Az útvonal nem írható."
|
data["StorageError"] = "Az útvonal nem írható."
|
||||||
s.executeTemplate(w, r, "settings", data)
|
s.executeTemplate(w, r, "storage", data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1524,7 +1509,7 @@ func (s *Server) settingsStorageAddHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
for _, existing := range s.settings.GetStoragePaths() {
|
for _, existing := range s.settings.GetStoragePaths() {
|
||||||
if system.PathsOverlap(path, existing.Path) {
|
if system.PathsOverlap(path, existing.Path) {
|
||||||
data["StorageError"] = fmt.Sprintf("Az útvonal átfedi a már regisztrált %s útvonalat.", 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
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1545,7 +1530,7 @@ func (s *Server) settingsStorageAddHandler(w http.ResponseWriter, r *http.Reques
|
|||||||
if err := s.settings.AddStoragePath(sp); err != nil {
|
if err := s.settings.AddStoragePath(sp); err != nil {
|
||||||
s.logger.Printf("[ERROR] [web] Failed to add storage path: %v", err)
|
s.logger.Printf("[ERROR] [web] Failed to add storage path: %v", err)
|
||||||
data["StorageError"] = "Hiba a mentés során."
|
data["StorageError"] = "Hiba a mentés során."
|
||||||
s.executeTemplate(w, r, "settings", data)
|
s.executeTemplate(w, r, "storage", data)
|
||||||
return
|
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)
|
s.logger.Printf("[DEBUG] [web] settingsStorageRemoveHandler: path=%s from %s", path, r.RemoteAddr)
|
||||||
}
|
}
|
||||||
|
|
||||||
data := s.settingsData()
|
data := s.storagePageData()
|
||||||
|
|
||||||
// Check: apps using this path
|
// Check: apps using this path
|
||||||
apps := s.appsUsingPath(path)
|
apps := s.appsUsingPath(path)
|
||||||
if len(apps) > 0 {
|
if len(apps) > 0 {
|
||||||
data["StorageError"] = fmt.Sprintf("Nem törölhető: az alábbi alkalmazások használják: %s", strings.Join(apps, ", "))
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1576,7 +1561,7 @@ func (s *Server) settingsStorageRemoveHandler(w http.ResponseWriter, r *http.Req
|
|||||||
for _, sp := range s.settings.GetStoragePaths() {
|
for _, sp := range s.settings.GetStoragePaths() {
|
||||||
if sp.Path == path && sp.IsDefault {
|
if sp.Path == path && sp.IsDefault {
|
||||||
data["StorageError"] = "Az alapértelmezett adattároló nem törölhető."
|
data["StorageError"] = "Az alapértelmezett adattároló nem törölhető."
|
||||||
s.executeTemplate(w, r, "settings", data)
|
s.executeTemplate(w, r, "storage", data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1584,13 +1569,13 @@ func (s *Server) settingsStorageRemoveHandler(w http.ResponseWriter, r *http.Req
|
|||||||
// Check: last path
|
// Check: last path
|
||||||
if len(s.settings.GetStoragePaths()) <= 1 {
|
if len(s.settings.GetStoragePaths()) <= 1 {
|
||||||
data["StorageError"] = "Az utolsó adattároló nem törölhető."
|
data["StorageError"] = "Az utolsó adattároló nem törölhető."
|
||||||
s.executeTemplate(w, r, "settings", data)
|
s.executeTemplate(w, r, "storage", data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.settings.RemoveStoragePath(path); err != nil {
|
if err := s.settings.RemoveStoragePath(path); err != nil {
|
||||||
data["StorageError"] = "Hiba a törlés során."
|
data["StorageError"] = "Hiba a törlés során."
|
||||||
s.executeTemplate(w, r, "settings", data)
|
s.executeTemplate(w, r, "storage", data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1645,17 +1630,17 @@ func (s *Server) settingsStorageLabelHandler(w http.ResponseWriter, r *http.Requ
|
|||||||
}
|
}
|
||||||
|
|
||||||
if label == "" || len(label) > 50 {
|
if label == "" || len(label) > 50 {
|
||||||
data := s.settingsData()
|
data := s.storagePageData()
|
||||||
data["StorageError"] = "A megnevezés nem lehet üres és legfeljebb 50 karakter."
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.settings.SetStorageLabel(path, label); err != nil {
|
if err := s.settings.SetStorageLabel(path, label); err != nil {
|
||||||
s.logger.Printf("[ERROR] [web] Failed to set storage label: %v", err)
|
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."
|
data["StorageError"] = "Hiba a megnevezés mentésekor."
|
||||||
s.executeTemplate(w, r, "settings", data)
|
s.executeTemplate(w, r, "storage", data)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,15 +50,80 @@ func getPage(t *testing.T, s *Server, path string) *httptest.ResponseRecorder {
|
|||||||
return rec
|
return rec
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSettingsSplitPagesRender (D1 Scenario A skeleton): the four pages respond 200.
|
// TestSettingsSplitPagesRender (D1 Scenario A): each page responds 200, carries its own
|
||||||
// Per-page unique-section markers are asserted once the template split lands (Part 2).
|
// sections, and does NOT carry another page's sections (no cross-leak).
|
||||||
func TestSettingsSplitPagesRender(t *testing.T) {
|
func TestSettingsSplitPagesRender(t *testing.T) {
|
||||||
s := testPageServer(t)
|
s := testPageServer(t)
|
||||||
for _, path := range []string{"/settings", "/settings/notifications", "/settings/security", "/storage"} {
|
cases := []struct {
|
||||||
rec := getPage(t, s, path)
|
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 {
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,12 +23,18 @@
|
|||||||
<ul class="nav-links">
|
<ul class="nav-links">
|
||||||
<li><a href="/" class="{{if eq .Page "dashboard"}}active{{end}}"><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
|
<li><a href="/" class="{{if eq .Page "dashboard"}}active{{end}}"><svg class="ico"><use href="#i-layout-grid"/></svg>Vezérlőpult</a></li>
|
||||||
<li><a href="/stacks" class="{{if eq .Page "stacks"}}active{{end}}"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
|
<li><a href="/stacks" class="{{if eq .Page "stacks"}}active{{end}}"><svg class="ico"><use href="#i-cloud"/></svg>Alkalmazások</a></li>
|
||||||
|
<li><a href="/storage" class="{{if eq .Page "storage"}}active{{end}}"><svg class="ico"><use href="#i-hard-drive"/></svg>Tárhely</a></li>
|
||||||
<li><a href="/backups" class="{{if eq .Page "backups"}}active{{end}}"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés</a></li>
|
<li><a href="/backups" class="{{if eq .Page "backups"}}active{{end}}"><svg class="ico"><use href="#i-shield"/></svg>Biztonsági mentés</a></li>
|
||||||
<li><a href="/monitoring" class="{{if eq .Page "monitoring"}}active{{end}}"><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
|
<li><a href="/monitoring" class="{{if eq .Page "monitoring"}}active{{end}}"><svg class="ico"><use href="#i-cpu"/></svg>Rendszermonitor</a></li>
|
||||||
{{if .DebugMode}}<li><a href="/debug" class="{{if eq .Page "debug"}}active{{end}}"><svg class="ico"><use href="#i-wrench"/></svg>Debug</a></li>{{end}}
|
{{if .DebugMode}}<li><a href="/debug" class="{{if eq .Page "debug"}}active{{end}}"><svg class="ico"><use href="#i-wrench"/></svg>Debug</a></li>{{end}}
|
||||||
</ul>
|
</ul>
|
||||||
<div class="sidebar-bottom">
|
<div class="sidebar-bottom">
|
||||||
<a href="/settings" class="sidebar-settings-link {{if eq .Page "settings"}}active{{end}}"><svg class="ico"><use href="#i-settings"/></svg>Beállítások</a>
|
<div class="nav-group-label">Beállítások</div>
|
||||||
|
<ul class="nav-links nav-links-sub">
|
||||||
|
<li><a href="/settings" class="{{if eq .Page "settings"}}active{{end}}"><svg class="ico"><use href="#i-settings"/></svg>Rendszer</a></li>
|
||||||
|
<li><a href="/settings/notifications" class="{{if eq .Page "settings-notifications"}}active{{end}}"><svg class="ico"><use href="#i-bell"/></svg>Értesítések</a></li>
|
||||||
|
<li><a href="/settings/security" class="{{if eq .Page "settings-security"}}active{{end}}"><svg class="ico"><use href="#i-lock"/></svg>Biztonság és hozzáférés</a></li>
|
||||||
|
</ul>
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
<span class="version">{{.Version}}</span>
|
<span class="version">{{.Version}}</span>
|
||||||
{{if .AuthEnabled}}<a href="/logout" class="logout-link">Kijelentkezés ↗</a>{{end}}
|
{{if .AuthEnabled}}<a href="/logout" class="logout-link">Kijelentkezés ↗</a>{{end}}
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
{{define "settings_notifications"}}
|
||||||
|
{{template "layout_start" .}}
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>Beállítások — Értesítések</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section C: Notification Preferences -->
|
||||||
|
<div class="settings-card">
|
||||||
|
<h3>Értesítések</h3>
|
||||||
|
{{if .HubEnabled}}
|
||||||
|
{{if .NotificationSuccess}}<div class="alert alert-info">{{.NotificationSuccess}}</div>{{end}}
|
||||||
|
{{if .NotificationError}}<div class="alert alert-error">{{.NotificationError}}</div>{{end}}
|
||||||
|
<form method="POST" action="/settings/notifications">
|
||||||
|
{{.CSRFField}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="notification_email">E-mail cím</label>
|
||||||
|
<input type="email" id="notification_email" name="notification_email"
|
||||||
|
value="{{with .NotificationPrefs}}{{.Email}}{{end}}"
|
||||||
|
placeholder="pelda@email.hu" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Hibák és figyelmeztetések:</label>
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" name="event_backup_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "backup_failed"}}checked{{end}}{{end}}{{end}}>
|
||||||
|
<span class="toggle-label">Biztonsági mentés sikertelen</span>
|
||||||
|
</label>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" name="event_db_dump_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "db_dump_failed"}}checked{{end}}{{end}}{{end}}>
|
||||||
|
<span class="toggle-label">Adatbázis mentés sikertelen</span>
|
||||||
|
</label>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" name="event_backup_integrity_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "backup_integrity_failed"}}checked{{end}}{{end}}{{end}}>
|
||||||
|
<span class="toggle-label">Mentés sérülés észlelve</span>
|
||||||
|
</label>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" name="event_crossdrive_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "crossdrive_failed"}}checked{{end}}{{end}}{{end}}>
|
||||||
|
<span class="toggle-label">Másodlagos mentés sikertelen</span>
|
||||||
|
</label>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" name="event_disk_alerts" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "disk_warning"}}checked{{end}}{{end}}{{end}}>
|
||||||
|
<span class="toggle-label">Lemez figyelmeztetés (90%+)</span>
|
||||||
|
</label>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" name="event_storage_disconnected" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "storage_disconnected"}}checked{{end}}{{end}}{{end}}>
|
||||||
|
<span class="toggle-label">Meghajtó leválasztva</span>
|
||||||
|
</label>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" name="event_node_down" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "node_down"}}checked{{end}}{{end}}{{end}}>
|
||||||
|
<span class="toggle-label">Szerver nem elérhető</span>
|
||||||
|
</label>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" name="event_health_critical" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "health_critical"}}checked{{end}}{{end}}{{end}}>
|
||||||
|
<span class="toggle-label">Rendszer állapot kritikus</span>
|
||||||
|
</label>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" name="event_expected_missed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "expected_backup_missed"}}checked{{end}}{{end}}{{end}}>
|
||||||
|
<span class="toggle-label">Elvárt mentés elmaradt</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Tájékoztató:</label>
|
||||||
|
<div class="checkbox-group">
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" name="event_storage_reconnected" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "storage_reconnected"}}checked{{end}}{{end}}{{end}}>
|
||||||
|
<span class="toggle-label">Meghajtó újra csatlakoztatva</span>
|
||||||
|
</label>
|
||||||
|
<label class="toggle">
|
||||||
|
<input type="checkbox" name="event_health_recovered" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "health_recovered"}}checked{{end}}{{end}}{{end}}>
|
||||||
|
<span class="toggle-label">Rendszer állapot helyreállt</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="cooldown_hours">Értesítési szünet</label>
|
||||||
|
<div class="form-inline">
|
||||||
|
<input type="number" id="cooldown_hours" name="cooldown_hours" min="1" max="168"
|
||||||
|
value="{{with .NotificationPrefs}}{{.CooldownHours}}{{end}}"
|
||||||
|
class="form-control form-control-narrow">
|
||||||
|
<span class="form-hint">óra (azonos probléma esetén ennyi ideig nem küld újat)</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">Mentés</button>
|
||||||
|
<button type="submit" formaction="/settings/notifications/test" class="btn btn-outline">Teszt email küldése</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{else}}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
Az értesítések a központi rendszeren keresztül működnek, ami jelenleg nincs bekapcsolva.
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section: App email (SMTP relay) -->
|
||||||
|
{{if .AppEmailAvailable}}
|
||||||
|
<div class="settings-card">
|
||||||
|
<h3>Alkalmazás-email</h3>
|
||||||
|
<p class="settings-card-desc">
|
||||||
|
Az alkalmazások a Felhom-on keresztül küldhetnek emailt (pl. jelszó-visszaállítás, meghívók),
|
||||||
|
külön email-szolgáltató beállítása nélkül. A feladó címe minden alkalmazásnál a saját
|
||||||
|
<em><alkalmazás>@felhom.eu</em> címe lesz.
|
||||||
|
</p>
|
||||||
|
{{if .AppEmailSuccess}}<div class="alert alert-info">{{.AppEmailSuccess}}</div>{{end}}
|
||||||
|
{{if .AppEmailError}}<div class="alert alert-error">{{.AppEmailError}}</div>{{end}}
|
||||||
|
<form method="POST" action="/settings/app-email">
|
||||||
|
{{.CSRFField}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label style="display:flex;align-items:center;gap:.5rem">
|
||||||
|
<input type="checkbox" name="app_email_enabled" value="on" {{if .AppEmailEnabled}}checked{{end}}>
|
||||||
|
Alkalmazás-email engedélyezése
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="app_email_from_name">Feladó neve (opcionális)</label>
|
||||||
|
<input type="text" id="app_email_from_name" name="app_email_from_name"
|
||||||
|
value="{{.AppEmailFromName}}" placeholder="pl. a háztartás neve" class="form-control">
|
||||||
|
<span class="form-hint">Ez jelenik meg a kimenő emailek feladójaként az alkalmazás neve mellett.</span>
|
||||||
|
</div>
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary">Mentés</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
<p class="settings-card-desc" style="margin-top:.75rem">
|
||||||
|
Bekapcsolás után az egyes alkalmazásoknál is engedélyezni kell az email-küldést (az alkalmazás oldalán).
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{template "layout_end" .}}
|
||||||
|
{{end}}
|
||||||
@@ -0,0 +1,421 @@
|
|||||||
|
{{define "settings_security"}}
|
||||||
|
{{template "layout_start" .}}
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>Beállítások — Biztonság és hozzáférés</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section B: Password Change -->
|
||||||
|
<div class="settings-card">
|
||||||
|
<h3>Jelszó módosítás</h3>
|
||||||
|
{{if .AuthEnabled}}
|
||||||
|
{{if .PasswordError}}<div class="alert alert-error">{{.PasswordError}}</div>{{end}}
|
||||||
|
<form method="POST" action="/settings/password">
|
||||||
|
{{.CSRFField}}
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="current_password">Jelenlegi jelszó</label>
|
||||||
|
<input type="password" id="current_password" name="current_password" required
|
||||||
|
placeholder="Adja meg a jelenlegi jelszavát" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="new_password">Új jelszó</label>
|
||||||
|
<input type="password" id="new_password" name="new_password" required minlength="8"
|
||||||
|
placeholder="Legalább 8 karakter" class="form-control">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="confirm_password">Új jelszó megerősítése</label>
|
||||||
|
<input type="password" id="confirm_password" name="confirm_password" required minlength="8"
|
||||||
|
placeholder="Jelszó mégegyszer" class="form-control">
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn btn-primary">Jelszó módosítása</button>
|
||||||
|
</form>
|
||||||
|
{{else}}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
A jelszavas védelem nincs beállítva. Kérd az üzemeltetőt a beállításhoz.
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section: Geo-Restriction -->
|
||||||
|
<div class="settings-card">
|
||||||
|
<h3>Földrajzi korlátozás</h3>
|
||||||
|
<p class="settings-card-desc">
|
||||||
|
Ország alapján korlátozható a webes alkalmazások elérése a Cloudflare WAF segítségével.
|
||||||
|
<br><span class="form-hint">A helyi hálózati hozzáférés mindig engedélyezett (nem halad át a Cloudflare-en).</span>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{{if not .CFConfigured}}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
A Cloudflare API token nincs konfigurálva. Kérd az üzemeltetőt a beállításhoz.<br>
|
||||||
|
<small>A tokennek <strong>Zone WAF:Edit</strong> jogosultsággal kell rendelkeznie.</small>
|
||||||
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div id="geo-status-msg"></div>
|
||||||
|
|
||||||
|
<label class="toggle" style="margin-bottom:1rem">
|
||||||
|
<input type="checkbox" id="geo-enabled" {{if .GeoEnabled}}checked{{end}}
|
||||||
|
onchange="toggleGeo(this.checked)">
|
||||||
|
<span class="toggle-label">Geo-korlátozás aktív</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div id="geo-details" {{if not .GeoEnabled}}style="display:none"{{end}}>
|
||||||
|
<!-- Global allowed countries -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label>Engedélyezett országok (globális)</label>
|
||||||
|
<div class="geo-country-selector" id="geo-countries">
|
||||||
|
<input type="text" id="geo-search" class="form-control"
|
||||||
|
placeholder="Ország keresése..."
|
||||||
|
autocomplete="off"
|
||||||
|
oninput="filterCountries(this.value)"
|
||||||
|
onfocus="showCountryList()"
|
||||||
|
onblur="setTimeout(function(){hideCountryList()},200)">
|
||||||
|
<div class="geo-country-list" id="geo-country-list"></div>
|
||||||
|
</div>
|
||||||
|
<div class="geo-selected-tags" id="geo-selected-tags"></div>
|
||||||
|
<span class="form-hint">Csak a kiválasztott országokból érhető el a rendszer.</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Per-app overrides -->
|
||||||
|
<div class="form-group" style="margin-top:1.5rem">
|
||||||
|
<label>Alkalmazás-specifikus felülírások</label>
|
||||||
|
<div id="geo-app-overrides"></div>
|
||||||
|
{{if .DeployedApps}}
|
||||||
|
<div style="margin-top:.5rem;display:flex;align-items:center;gap:.5rem">
|
||||||
|
<select id="geo-add-app-select" class="form-control" style="max-width:250px">
|
||||||
|
<option value="">— Alkalmazás kiválasztása —</option>
|
||||||
|
{{range .DeployedApps}}
|
||||||
|
<option value="{{.Name}}">{{.Display}}</option>
|
||||||
|
{{end}}
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-sm btn-outline" onclick="addAppOverride()">+ Hozzáadás</button>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sync status & save -->
|
||||||
|
<div class="form-group" style="margin-top:1.5rem">
|
||||||
|
<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
|
||||||
|
<button class="btn btn-primary" id="btn-geo-save" onclick="saveGeoSettings()">
|
||||||
|
Mentés és szinkronizálás
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-sm btn-outline" onclick="triggerGeoSync()">Kézi szinkronizálás</button>
|
||||||
|
<span id="geo-sync-status" class="form-hint">
|
||||||
|
{{if .GeoLastSync}}Utolsó szinkronizálás: {{.GeoLastSync}}{{end}}
|
||||||
|
{{if .GeoLastError}} <span class="state-text-crit">{{.GeoLastError}}</span>{{end}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
// Geo-restriction UI state
|
||||||
|
var allCountries = [];
|
||||||
|
var selectedCountries = {{json .GeoAllowedCountries}};
|
||||||
|
var appOverrides = {{json .GeoAppOverrides}};
|
||||||
|
|
||||||
|
// Load countries list on first use
|
||||||
|
function ensureCountries(cb) {
|
||||||
|
if (allCountries.length > 0) { cb(); return; }
|
||||||
|
fetch('/api/geo/countries', {headers: csrfHeaders()})
|
||||||
|
.then(function(r){return r.json()})
|
||||||
|
.then(function(d){
|
||||||
|
if (d.ok) allCountries = d.data;
|
||||||
|
cb();
|
||||||
|
})
|
||||||
|
.catch(function(){ cb(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
window.toggleGeo = function(enabled) {
|
||||||
|
document.getElementById('geo-details').style.display = enabled ? '' : 'none';
|
||||||
|
if (enabled) ensureCountries(renderTags);
|
||||||
|
};
|
||||||
|
|
||||||
|
window.showCountryList = function() {
|
||||||
|
ensureCountries(function(){ filterCountries(document.getElementById('geo-search').value); });
|
||||||
|
};
|
||||||
|
|
||||||
|
window.hideCountryList = function() {
|
||||||
|
document.getElementById('geo-country-list').style.display = 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
window.filterCountries = function(query) {
|
||||||
|
var list = document.getElementById('geo-country-list');
|
||||||
|
var q = query.toLowerCase();
|
||||||
|
var html = '';
|
||||||
|
var count = 0;
|
||||||
|
for (var i = 0; i < allCountries.length && count < 15; i++) {
|
||||||
|
var c = allCountries[i];
|
||||||
|
if (selectedCountries.indexOf(c.code) >= 0) continue;
|
||||||
|
if (q && c.name.toLowerCase().indexOf(q) < 0 && c.code.toLowerCase().indexOf(q) < 0) continue;
|
||||||
|
html += '<div class="geo-country-option" onmousedown="addCountry(\'' + c.code + '\',\'' + escHtml(c.name) + '\')">'
|
||||||
|
+ escHtml(c.name) + ' <small>(' + c.code + ')</small></div>';
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
list.innerHTML = html || '<div class="geo-country-option" style="opacity:.5">Nincs találat</div>';
|
||||||
|
// Reveal with 'block', NOT '' — the .geo-country-list CSS default is display:none,
|
||||||
|
// and clearing the inline style ('') would fall back to that and keep the (populated)
|
||||||
|
// list hidden. This was the country-autocomplete "no list" bug.
|
||||||
|
list.style.display = count > 0 || q ? 'block' : 'none';
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addCountry = function(code, name) {
|
||||||
|
if (selectedCountries.indexOf(code) >= 0) return;
|
||||||
|
selectedCountries.push(code);
|
||||||
|
renderTags();
|
||||||
|
document.getElementById('geo-search').value = '';
|
||||||
|
hideCountryList();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.removeCountry = function(code) {
|
||||||
|
if (code === 'HU') {
|
||||||
|
if (!confirm('Figyelem: Magyarország eltávolítása azt jelenti, hogy magyar IP-ről sem lesz elérhető a rendszer távolról. Biztosan folytatja?')) return;
|
||||||
|
}
|
||||||
|
selectedCountries = selectedCountries.filter(function(c){return c !== code});
|
||||||
|
renderTags();
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderTags() {
|
||||||
|
var el = document.getElementById('geo-selected-tags');
|
||||||
|
var html = '';
|
||||||
|
for (var i = 0; i < selectedCountries.length; i++) {
|
||||||
|
var code = selectedCountries[i];
|
||||||
|
var name = countryName(code);
|
||||||
|
var isHU = code === 'HU' ? ' geo-tag-hu' : '';
|
||||||
|
html += '<span class="geo-tag' + isHU + '">'
|
||||||
|
+ escHtml(name) + ' (' + code + ') '
|
||||||
|
+ '<span class="geo-tag-remove" onclick="removeCountry(\'' + code + '\')">×</span>'
|
||||||
|
+ '</span>';
|
||||||
|
}
|
||||||
|
el.innerHTML = html;
|
||||||
|
renderAppOverrides();
|
||||||
|
}
|
||||||
|
|
||||||
|
function countryName(code) {
|
||||||
|
for (var i = 0; i < allCountries.length; i++) {
|
||||||
|
if (allCountries[i].code === code) return allCountries[i].name;
|
||||||
|
}
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Per-app overrides ---
|
||||||
|
window.addAppOverride = function() {
|
||||||
|
var sel = document.getElementById('geo-add-app-select');
|
||||||
|
var appName = sel.value;
|
||||||
|
if (!appName) return;
|
||||||
|
if (!appOverrides) appOverrides = {};
|
||||||
|
if (appOverrides[appName]) { sel.value = ''; return; }
|
||||||
|
// Default: same countries as global
|
||||||
|
appOverrides[appName] = {allowed_countries: selectedCountries.slice()};
|
||||||
|
sel.value = '';
|
||||||
|
renderAppOverrides();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.removeAppOverride = function(appName) {
|
||||||
|
delete appOverrides[appName];
|
||||||
|
renderAppOverrides();
|
||||||
|
};
|
||||||
|
|
||||||
|
window.toggleAppCountry = function(appName, code, el) {
|
||||||
|
var ov = appOverrides[appName];
|
||||||
|
if (!ov) return;
|
||||||
|
var idx = ov.allowed_countries.indexOf(code);
|
||||||
|
if (idx >= 0) {
|
||||||
|
if (code === 'HU' && !confirm('Magyarország eltávolítása nem ajánlott. Folytatja?')) {
|
||||||
|
el.checked = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
ov.allowed_countries.splice(idx, 1);
|
||||||
|
} else {
|
||||||
|
ov.allowed_countries.push(code);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function renderAppOverrides() {
|
||||||
|
var el = document.getElementById('geo-app-overrides');
|
||||||
|
if (!appOverrides || Object.keys(appOverrides).length === 0) {
|
||||||
|
el.innerHTML = '<p class="form-hint">Nincs alkalmazás-specifikus beállítás. Minden alkalmazás a globális beállítást követi.</p>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var html = '';
|
||||||
|
for (var appName in appOverrides) {
|
||||||
|
var ov = appOverrides[appName];
|
||||||
|
var displayName = appName;
|
||||||
|
// Try to find display name from select
|
||||||
|
var opts = document.getElementById('geo-add-app-select');
|
||||||
|
if (opts) {
|
||||||
|
for (var j = 0; j < opts.options.length; j++) {
|
||||||
|
if (opts.options[j].value === appName) { displayName = opts.options[j].text; break; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
html += '<div class="geo-app-override-row">';
|
||||||
|
html += '<strong>' + escHtml(displayName) + '</strong>';
|
||||||
|
html += '<div class="geo-selected-tags" style="flex:1;margin:0 .5rem">';
|
||||||
|
for (var i = 0; i < ov.allowed_countries.length; i++) {
|
||||||
|
var code = ov.allowed_countries[i];
|
||||||
|
html += '<span class="geo-tag geo-tag-sm">' + code + '</span>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
html += '<button class="btn btn-sm btn-outline" onclick="editAppOverride(\'' + appName + '\')">Szerkesztés</button>';
|
||||||
|
html += '<button class="btn btn-sm btn-danger-outline" onclick="removeAppOverride(\'' + appName + '\')">Törlés</button>';
|
||||||
|
html += '</div>';
|
||||||
|
}
|
||||||
|
el.innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.editAppOverride = function(appName) {
|
||||||
|
var ov = appOverrides[appName];
|
||||||
|
if (!ov) return;
|
||||||
|
ensureCountries(function(){
|
||||||
|
var checked = {};
|
||||||
|
for (var i = 0; i < ov.allowed_countries.length; i++) checked[ov.allowed_countries[i]] = true;
|
||||||
|
var html = '<div class="geo-edit-overlay" id="geo-edit-' + appName + '">';
|
||||||
|
html += '<h4>Engedélyezett országok: ' + escHtml(appName) + '</h4>';
|
||||||
|
html += '<div class="geo-edit-grid">';
|
||||||
|
for (var i = 0; i < allCountries.length; i++) {
|
||||||
|
var c = allCountries[i];
|
||||||
|
html += '<label class="geo-edit-item"><input type="checkbox" value="' + c.code + '"'
|
||||||
|
+ (checked[c.code] ? ' checked' : '') + ' onchange="toggleAppCountry(\'' + appName + '\',\'' + c.code + '\',this)">'
|
||||||
|
+ ' ' + escHtml(c.name) + ' (' + c.code + ')</label>';
|
||||||
|
}
|
||||||
|
html += '</div>';
|
||||||
|
html += '<button class="btn btn-sm btn-primary" style="margin-top:.5rem" onclick="closeAppEdit(\'' + appName + '\')">Kész</button>';
|
||||||
|
html += '</div>';
|
||||||
|
document.getElementById('geo-app-overrides').innerHTML += html;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.closeAppEdit = function(appName) {
|
||||||
|
var el = document.getElementById('geo-edit-' + appName);
|
||||||
|
if (el) el.remove();
|
||||||
|
renderAppOverrides();
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Save & Sync ---
|
||||||
|
window.saveGeoSettings = function() {
|
||||||
|
var btn = document.getElementById('btn-geo-save');
|
||||||
|
var status = document.getElementById('geo-status-msg');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Mentés...';
|
||||||
|
|
||||||
|
var payload = {
|
||||||
|
enabled: document.getElementById('geo-enabled').checked,
|
||||||
|
allowed_countries: selectedCountries
|
||||||
|
};
|
||||||
|
|
||||||
|
fetch('/api/geo/settings', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||||
|
body: JSON.stringify(payload)
|
||||||
|
})
|
||||||
|
.then(function(r){return r.json()})
|
||||||
|
.then(function(d){
|
||||||
|
if (d.ok) {
|
||||||
|
status.innerHTML = '<div class="alert alert-info">' + (d.message || 'Mentve') + '</div>';
|
||||||
|
// Save per-app overrides
|
||||||
|
if (appOverrides && Object.keys(appOverrides).length > 0) {
|
||||||
|
saveAllAppOverrides();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
status.innerHTML = '<div class="alert alert-error">' + (d.error || 'Hiba') + '</div>';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function(err){
|
||||||
|
status.innerHTML = '<div class="alert alert-error">Hálózati hiba</div>';
|
||||||
|
})
|
||||||
|
.finally(function(){
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Mentés és szinkronizálás';
|
||||||
|
setTimeout(function(){ status.innerHTML = ''; }, 8000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function saveAllAppOverrides() {
|
||||||
|
for (var appName in appOverrides) {
|
||||||
|
(function(name, ov){
|
||||||
|
fetch('/api/stacks/' + name + '/geo/override', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
||||||
|
body: JSON.stringify({allowed_countries: ov.allowed_countries})
|
||||||
|
});
|
||||||
|
})(appName, appOverrides[appName]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.triggerGeoSync = function() {
|
||||||
|
fetch('/api/geo/sync', {method:'POST', headers: csrfHeaders()})
|
||||||
|
.then(function(r){return r.json()})
|
||||||
|
.then(function(d){
|
||||||
|
var status = document.getElementById('geo-sync-status');
|
||||||
|
status.textContent = d.ok ? 'Szinkronizálás elindítva...' : (d.error || 'Hiba');
|
||||||
|
setTimeout(function(){
|
||||||
|
fetch('/api/geo/status', {headers: csrfHeaders()})
|
||||||
|
.then(function(r){return r.json()})
|
||||||
|
.then(function(d){
|
||||||
|
if (d.ok && d.data) {
|
||||||
|
var sync = d.data.last_sync || '';
|
||||||
|
var err = d.data.last_sync_error || '';
|
||||||
|
status.innerHTML = sync ? ('Utolsó: ' + sync.substring(0,19).replace('T',' ')) : '';
|
||||||
|
if (err) status.innerHTML += ' <span class="state-text-crit">' + escHtml(err) + '</span>';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 5000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
function escHtml(s) {
|
||||||
|
var d = document.createElement('div');
|
||||||
|
d.textContent = s;
|
||||||
|
return d.innerHTML;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize on load
|
||||||
|
if (document.getElementById('geo-enabled') && document.getElementById('geo-enabled').checked) {
|
||||||
|
ensureCountries(renderTags);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Section: Recovery Info -->
|
||||||
|
{{if .RetrievalPassword}}
|
||||||
|
<div class="settings-card">
|
||||||
|
<h3>Vészhelyzeti információk</h3>
|
||||||
|
<p class="settings-card-desc">
|
||||||
|
Ezeket az adatokat mentse el biztos helyre. Újratelepítés esetén szükség lesz rájuk a rendszer visszaállításához.
|
||||||
|
</p>
|
||||||
|
<div class="settings-grid">
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Ügyfél azonosító</span>
|
||||||
|
<span class="settings-value mono">{{.CustomerID}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Hub URL</span>
|
||||||
|
<span class="settings-value mono">{{.HubURL}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Visszaállítási jelszó</span>
|
||||||
|
<span class="settings-value">
|
||||||
|
<span id="retrieval-pw-hidden">••••••••••••••••
|
||||||
|
<button type="button" class="btn btn-xs btn-outline" onclick="document.getElementById('retrieval-pw-hidden').style.display='none';document.getElementById('retrieval-pw-visible').style.display='inline';">Megjelenít</button>
|
||||||
|
</span>
|
||||||
|
<span id="retrieval-pw-visible" style="display:none">
|
||||||
|
<code class="mono">{{.RetrievalPassword}}</code>
|
||||||
|
<button type="button" class="btn btn-xs btn-outline" onclick="document.getElementById('retrieval-pw-visible').style.display='none';document.getElementById('retrieval-pw-hidden').style.display='inline';">Elrejt</button>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Támogatás</span>
|
||||||
|
<span class="settings-value">
|
||||||
|
<a href="mailto:{{.SupportEmail}}" style="color: var(--blue);">{{.SupportEmail}}</a>
|
||||||
|
|
|
||||||
|
<a href="{{.SupportURL}}" target="_blank" style="color: var(--blue);">felhom.eu/kapcsolat</a>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{template "layout_end" .}}
|
||||||
|
{{end}}
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
{{define "settings_system"}}
|
||||||
|
{{template "layout_start" .}}
|
||||||
|
|
||||||
|
<div class="page-header">
|
||||||
|
<h2>Beállítások — Rendszer</h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section A: System Configuration (read-only) -->
|
||||||
|
<div class="settings-card">
|
||||||
|
<h3>Rendszer konfiguráció</h3>
|
||||||
|
<p class="settings-card-desc">Az üzemeltető által beállított értékek. Módosításhoz kérd az üzemeltetőt.</p>
|
||||||
|
<div class="settings-grid">
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Ügyfél azonosító</span>
|
||||||
|
<span class="settings-value mono">{{.CustomerID}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Ügyfél neve</span>
|
||||||
|
<span class="settings-value">{{.CustomerName}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Domain</span>
|
||||||
|
<span class="settings-value mono">{{.CustomerDomain}}</span>
|
||||||
|
</div>
|
||||||
|
{{if .GitRepoURL}}
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Alkalmazás sablon forrás</span>
|
||||||
|
<span class="settings-value mono settings-value-truncate">{{.GitRepoURL}}</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Sablon szinkronizálás</span>
|
||||||
|
<span class="settings-value mono">{{.GitSyncInterval}}</span>
|
||||||
|
</div>
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Biztonsági mentés</span>
|
||||||
|
<span class="settings-value">{{if .BackupEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Aktív</span>{{else}}<span class="state-text-neutral">Inaktív</span>{{end}}</span>
|
||||||
|
</div>
|
||||||
|
{{if .BackupEnabled}}
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Mentés ütemezés</span>
|
||||||
|
<span class="settings-value mono">{{.DBDumpSchedule}} / {{.ResticSchedule}}</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Monitoring</span>
|
||||||
|
<span class="settings-value">{{if .MonitoringEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Aktív</span>{{else}}<span class="state-text-neutral">Inaktív</span>{{end}}</span>
|
||||||
|
</div>
|
||||||
|
{{if .MonitoringEnabled}}
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Healthchecks URL</span>
|
||||||
|
<span class="settings-value mono settings-value-truncate">{{if .HealthchecksBase}}{{.HealthchecksBase}}{{else}}–{{end}}</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Hub jelentés</span>
|
||||||
|
<span class="settings-value">{{if .HubEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Aktív</span>{{else}}–{{end}}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section: Version & Update -->
|
||||||
|
<div class="settings-card">
|
||||||
|
<h3>Verzió és frissítés</h3>
|
||||||
|
<div class="settings-grid">
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Jelenlegi verzió</span>
|
||||||
|
<span class="settings-value mono">{{.Version}}</span>
|
||||||
|
</div>
|
||||||
|
{{if .SelfUpdateEnabled}}
|
||||||
|
{{if .LatestVersion}}
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Legújabb verzió</span>
|
||||||
|
<span class="settings-value mono">
|
||||||
|
{{.LatestVersion}}
|
||||||
|
{{if .UpdateAvailable}}
|
||||||
|
<span class="state-text-run" style="margin-left:0.5em;">● Frissítés elérhető</span>
|
||||||
|
{{else}}
|
||||||
|
<span style="margin-left:0.5em; color:var(--text-3);">— naprakész</span>
|
||||||
|
{{end}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{if .LastCheckTime}}
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Utolsó ellenőrzés</span>
|
||||||
|
<span class="settings-value mono">{{.LastCheckTime}}</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{if .LastCheckError}}
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Hiba</span>
|
||||||
|
<span class="settings-value state-text-crit">{{.LastCheckError}}</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Automatikus frissítés</span>
|
||||||
|
<span class="settings-value">
|
||||||
|
{{if .AutoUpdateEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Aktív</span> <span class="mono">({{.AutoUpdateTime}})</span>{{else}}–{{end}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{if .ControllerFloor}}
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Minimális verzió (üzemeltető)</span>
|
||||||
|
<span class="settings-value mono">
|
||||||
|
{{.ControllerFloor}}
|
||||||
|
<span style="margin-left:0.5em; color:#888;">— a rendszer automatikusan erre a verzióra frissít, ha régebbi</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{if .UpdateRunning}}
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Állapot</span>
|
||||||
|
<span class="settings-value state-text-progress" id="auto-update-status"><svg class="ico ico-sm ico-spin"><use href="#i-rotate-cw"/></svg> Frissítés folyamatban — a vezérlő hamarosan újraindul…</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
{{with .LastUpdateState}}
|
||||||
|
<div class="settings-row">
|
||||||
|
<span class="settings-label">Utolsó frissítés</span>
|
||||||
|
<span class="settings-value">
|
||||||
|
{{if eq .Status "success"}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Sikeres</span> ({{.PreviousVersion}} → {{.TargetVersion}})
|
||||||
|
{{else if eq .Status "failed"}}<span class="state-text-crit"><svg class="ico ico-sm"><use href="#i-x"/></svg> Sikertelen</span> — {{.Error}}
|
||||||
|
{{else if eq .Status "pending"}}<span class="state-text-progress"><svg class="ico ico-sm ico-spin"><use href="#i-rotate-cw"/></svg> Folyamatban</span>
|
||||||
|
{{end}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="settings-row" style="padding-top: 0.5em;">
|
||||||
|
<span class="settings-label"></span>
|
||||||
|
<span class="settings-value">
|
||||||
|
<button class="btn btn-secondary btn-sm" id="btn-check-update" onclick="checkUpdate()">Frissítés keresése</button>
|
||||||
|
{{if .UpdateAvailable}}
|
||||||
|
<button class="btn btn-primary btn-sm" id="btn-trigger-update" onclick="triggerUpdate()" style="margin-left:0.5em;">Frissítés telepítése</button>
|
||||||
|
{{end}}
|
||||||
|
<span id="update-status-msg" style="margin-left:0.5em; display:none;"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function checkUpdate() {
|
||||||
|
var btn = document.getElementById('btn-check-update');
|
||||||
|
var msg = document.getElementById('update-status-msg');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Ellenőrzés...';
|
||||||
|
msg.style.display = 'none';
|
||||||
|
fetch('/api/selfupdate/check', {method:'POST', headers: csrfHeaders()})
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.ok) {
|
||||||
|
location.reload();
|
||||||
|
} else {
|
||||||
|
msg.textContent = data.error || 'Hiba történt';
|
||||||
|
msg.style.display = 'inline';
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Frissítés keresése';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function() {
|
||||||
|
msg.textContent = 'Kapcsolódási hiba';
|
||||||
|
msg.style.display = 'inline';
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Frissítés keresése';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerUpdate() {
|
||||||
|
if (!confirm('Biztosan frissíti a controllert?\n\nA folyamat alatt a vezérlőpult rövid időre elérhetetlenné válik.')) return;
|
||||||
|
var btn = document.getElementById('btn-trigger-update');
|
||||||
|
var checkBtn = document.getElementById('btn-check-update');
|
||||||
|
var msg = document.getElementById('update-status-msg');
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Frissítés...';
|
||||||
|
if (checkBtn) checkBtn.disabled = true;
|
||||||
|
msg.textContent = 'Frissítés folyamatban...';
|
||||||
|
msg.style.display = 'inline';
|
||||||
|
fetch('/api/selfupdate/update', {method:'POST', headers: csrfHeaders()})
|
||||||
|
.then(function(r) { return r.json(); })
|
||||||
|
.then(function(data) {
|
||||||
|
if (data.ok) {
|
||||||
|
msg.textContent = 'Újraindulás...';
|
||||||
|
pollUntilBack();
|
||||||
|
} else {
|
||||||
|
msg.textContent = data.error || 'Hiba történt';
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = 'Frissítés telepítése';
|
||||||
|
if (checkBtn) checkBtn.disabled = false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function() {
|
||||||
|
msg.textContent = 'Kapcsolódási hiba';
|
||||||
|
pollUntilBack();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function pollUntilBack() {
|
||||||
|
var iv = setInterval(function() {
|
||||||
|
fetch('/api/health')
|
||||||
|
.then(function(r) {
|
||||||
|
if (r.ok) {
|
||||||
|
clearInterval(iv);
|
||||||
|
location.reload();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(function() {});
|
||||||
|
}, 3000);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If an update is already in flight when the page loads (e.g. a floor-driven AUTO-update kicked off by
|
||||||
|
// the hub, not a button click), surface the same restart-poll panel so the page recovers itself.
|
||||||
|
{{if .UpdateRunning}}
|
||||||
|
pollUntilBack();
|
||||||
|
{{end}}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<!-- Section: Controller restart (self-serve) — always available (not gated on RetrievalPassword) -->
|
||||||
|
<div class="settings-card">
|
||||||
|
<h3>Vezérlő újraindítása</h3>
|
||||||
|
<p class="settings-card-desc">
|
||||||
|
Ha a vezérlő hibásan működik, itt biztonságosan újraindíthatja — nem kell az egész szervert újraindítani.
|
||||||
|
Az alkalmazásai futnak tovább; csak a vezérlő indul újra (néhány másodperc).
|
||||||
|
</p>
|
||||||
|
<div id="restart-status"></div>
|
||||||
|
<button type="button" class="btn btn-outline" id="btn-restart-controller" onclick="restartController()">Vezérlő újraindítása</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Section: Full server (guest) restart — a deliberate maintenance affordance, sibling to the controller restart -->
|
||||||
|
<div class="settings-card">
|
||||||
|
<h3>Kiszolgáló újraindítása</h3>
|
||||||
|
<p class="settings-card-desc">
|
||||||
|
Az egész kiszolgáló (szerver) újraindítása. Minden alkalmazás rövid időre leáll, és a vezérlőpult kb. 30 másodpercig nem elérhető. Akkor használja, ha a teljes rendszer újraindítására van szükség — egyébként a fenti „Vezérlő újraindítása” elegendő.
|
||||||
|
</p>
|
||||||
|
<div id="server-restart-status"></div>
|
||||||
|
<button type="button" class="btn btn-outline" id="btn-restart-server" onclick="restartServer()">Kiszolgáló újraindítása</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
function restartController() {
|
||||||
|
if (!confirm('Biztosan újraindítja a vezérlőt? A művelet néhány másodpercig tart, és a felület rövid időre elérhetetlen lesz.')) return;
|
||||||
|
var btn = document.getElementById('btn-restart-controller');
|
||||||
|
var status = document.getElementById('restart-status');
|
||||||
|
btn.disabled = true;
|
||||||
|
status.innerHTML = '<div class="alert alert-info">Újraindítás folyamatban… újracsatlakozás…</div>';
|
||||||
|
fetch('/api/selfrestart', { method: 'POST', headers: csrfHeaders() })
|
||||||
|
.then(function(){ pollRestart(0); })
|
||||||
|
.catch(function(){ pollRestart(0); }); // connection may drop as the process exits — poll regardless
|
||||||
|
}
|
||||||
|
function restartServer() {
|
||||||
|
if (!confirm('Biztosan újraindítja a kiszolgálót? Az alkalmazások és a vezérlőpult kb. 30 másodpercre elérhetetlenné válnak.')) return;
|
||||||
|
var btn = document.getElementById('btn-restart-server');
|
||||||
|
var status = document.getElementById('server-restart-status');
|
||||||
|
if (btn) btn.disabled = true;
|
||||||
|
if (status) status.innerHTML = '<div class="alert alert-info">Újraindítás folyamatban… a vezérlőpult néhány másodperc múlva újratölt.</div>';
|
||||||
|
fetch('/api/server/reboot', { method: 'POST', headers: csrfHeaders() })
|
||||||
|
.then(function(){ pollRestart(0); })
|
||||||
|
.catch(function(){ pollRestart(0); }); // the guest reboot drops the connection — poll regardless
|
||||||
|
}
|
||||||
|
function pollRestart(attempt) {
|
||||||
|
if (attempt > 60) { // ~2 min cap — never leave the user on a dead page silently
|
||||||
|
document.getElementById('restart-status').innerHTML =
|
||||||
|
'<div class="alert alert-error">Az újraindítás a vártnál tovább tart. Töltse újra az oldalt kézzel.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setTimeout(function(){
|
||||||
|
fetch('/', { method: 'GET', cache: 'no-store' })
|
||||||
|
.then(function(r){ if (r.ok) { window.location.reload(); } else { pollRestart(attempt + 1); } })
|
||||||
|
.catch(function(){ pollRestart(attempt + 1); });
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
{{template "layout_end" .}}
|
||||||
|
{{end}}
|
||||||
+9
-809
@@ -1,220 +1,10 @@
|
|||||||
{{define "settings"}}
|
{{define "storage"}}
|
||||||
{{template "layout_start" .}}
|
{{template "layout_start" .}}
|
||||||
|
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<h2>Beállítások</h2>
|
<h2>Tárhely</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Section A: System Configuration (read-only) -->
|
|
||||||
<div class="settings-card">
|
|
||||||
<h3>Rendszer konfiguráció</h3>
|
|
||||||
<p class="settings-card-desc">Az üzemeltető által beállított értékek. Módosításhoz kérd az üzemeltetőt.</p>
|
|
||||||
<div class="settings-grid">
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Ügyfél azonosító</span>
|
|
||||||
<span class="settings-value mono">{{.CustomerID}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Ügyfél neve</span>
|
|
||||||
<span class="settings-value">{{.CustomerName}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Domain</span>
|
|
||||||
<span class="settings-value mono">{{.CustomerDomain}}</span>
|
|
||||||
</div>
|
|
||||||
{{if .GitRepoURL}}
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Alkalmazás sablon forrás</span>
|
|
||||||
<span class="settings-value mono settings-value-truncate">{{.GitRepoURL}}</span>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Sablon szinkronizálás</span>
|
|
||||||
<span class="settings-value mono">{{.GitSyncInterval}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Biztonsági mentés</span>
|
|
||||||
<span class="settings-value">{{if .BackupEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Aktív</span>{{else}}<span class="state-text-neutral">Inaktív</span>{{end}}</span>
|
|
||||||
</div>
|
|
||||||
{{if .BackupEnabled}}
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Mentés ütemezés</span>
|
|
||||||
<span class="settings-value mono">{{.DBDumpSchedule}} / {{.ResticSchedule}}</span>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Monitoring</span>
|
|
||||||
<span class="settings-value">{{if .MonitoringEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Aktív</span>{{else}}<span class="state-text-neutral">Inaktív</span>{{end}}</span>
|
|
||||||
</div>
|
|
||||||
{{if .MonitoringEnabled}}
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Healthchecks URL</span>
|
|
||||||
<span class="settings-value mono settings-value-truncate">{{if .HealthchecksBase}}{{.HealthchecksBase}}{{else}}–{{end}}</span>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Hub jelentés</span>
|
|
||||||
<span class="settings-value">{{if .HubEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Aktív</span>{{else}}–{{end}}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Section: Version & Update -->
|
|
||||||
<div class="settings-card">
|
|
||||||
<h3>Verzió és frissítés</h3>
|
|
||||||
<div class="settings-grid">
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Jelenlegi verzió</span>
|
|
||||||
<span class="settings-value mono">{{.Version}}</span>
|
|
||||||
</div>
|
|
||||||
{{if .SelfUpdateEnabled}}
|
|
||||||
{{if .LatestVersion}}
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Legújabb verzió</span>
|
|
||||||
<span class="settings-value mono">
|
|
||||||
{{.LatestVersion}}
|
|
||||||
{{if .UpdateAvailable}}
|
|
||||||
<span class="state-text-run" style="margin-left:0.5em;">● Frissítés elérhető</span>
|
|
||||||
{{else}}
|
|
||||||
<span style="margin-left:0.5em; color:var(--text-3);">— naprakész</span>
|
|
||||||
{{end}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
{{if .LastCheckTime}}
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Utolsó ellenőrzés</span>
|
|
||||||
<span class="settings-value mono">{{.LastCheckTime}}</span>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
{{if .LastCheckError}}
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Hiba</span>
|
|
||||||
<span class="settings-value state-text-crit">{{.LastCheckError}}</span>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Automatikus frissítés</span>
|
|
||||||
<span class="settings-value">
|
|
||||||
{{if .AutoUpdateEnabled}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Aktív</span> <span class="mono">({{.AutoUpdateTime}})</span>{{else}}–{{end}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{{if .ControllerFloor}}
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Minimális verzió (üzemeltető)</span>
|
|
||||||
<span class="settings-value mono">
|
|
||||||
{{.ControllerFloor}}
|
|
||||||
<span style="margin-left:0.5em; color:#888;">— a rendszer automatikusan erre a verzióra frissít, ha régebbi</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
{{if .UpdateRunning}}
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Állapot</span>
|
|
||||||
<span class="settings-value state-text-progress" id="auto-update-status"><svg class="ico ico-sm ico-spin"><use href="#i-rotate-cw"/></svg> Frissítés folyamatban — a vezérlő hamarosan újraindul…</span>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
{{with .LastUpdateState}}
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Utolsó frissítés</span>
|
|
||||||
<span class="settings-value">
|
|
||||||
{{if eq .Status "success"}}<span class="state-text-run"><svg class="ico ico-sm"><use href="#i-check"/></svg> Sikeres</span> ({{.PreviousVersion}} → {{.TargetVersion}})
|
|
||||||
{{else if eq .Status "failed"}}<span class="state-text-crit"><svg class="ico ico-sm"><use href="#i-x"/></svg> Sikertelen</span> — {{.Error}}
|
|
||||||
{{else if eq .Status "pending"}}<span class="state-text-progress"><svg class="ico ico-sm ico-spin"><use href="#i-rotate-cw"/></svg> Folyamatban</span>
|
|
||||||
{{end}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
<div class="settings-row" style="padding-top: 0.5em;">
|
|
||||||
<span class="settings-label"></span>
|
|
||||||
<span class="settings-value">
|
|
||||||
<button class="btn btn-secondary btn-sm" id="btn-check-update" onclick="checkUpdate()">Frissítés keresése</button>
|
|
||||||
{{if .UpdateAvailable}}
|
|
||||||
<button class="btn btn-primary btn-sm" id="btn-trigger-update" onclick="triggerUpdate()" style="margin-left:0.5em;">Frissítés telepítése</button>
|
|
||||||
{{end}}
|
|
||||||
<span id="update-status-msg" style="margin-left:0.5em; display:none;"></span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function checkUpdate() {
|
|
||||||
var btn = document.getElementById('btn-check-update');
|
|
||||||
var msg = document.getElementById('update-status-msg');
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = 'Ellenőrzés...';
|
|
||||||
msg.style.display = 'none';
|
|
||||||
fetch('/api/selfupdate/check', {method:'POST', headers: csrfHeaders()})
|
|
||||||
.then(function(r) { return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (data.ok) {
|
|
||||||
location.reload();
|
|
||||||
} else {
|
|
||||||
msg.textContent = data.error || 'Hiba történt';
|
|
||||||
msg.style.display = 'inline';
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = 'Frissítés keresése';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function() {
|
|
||||||
msg.textContent = 'Kapcsolódási hiba';
|
|
||||||
msg.style.display = 'inline';
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = 'Frissítés keresése';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function triggerUpdate() {
|
|
||||||
if (!confirm('Biztosan frissíti a controllert?\n\nA folyamat alatt a vezérlőpult rövid időre elérhetetlenné válik.')) return;
|
|
||||||
var btn = document.getElementById('btn-trigger-update');
|
|
||||||
var checkBtn = document.getElementById('btn-check-update');
|
|
||||||
var msg = document.getElementById('update-status-msg');
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = 'Frissítés...';
|
|
||||||
if (checkBtn) checkBtn.disabled = true;
|
|
||||||
msg.textContent = 'Frissítés folyamatban...';
|
|
||||||
msg.style.display = 'inline';
|
|
||||||
fetch('/api/selfupdate/update', {method:'POST', headers: csrfHeaders()})
|
|
||||||
.then(function(r) { return r.json(); })
|
|
||||||
.then(function(data) {
|
|
||||||
if (data.ok) {
|
|
||||||
msg.textContent = 'Újraindulás...';
|
|
||||||
pollUntilBack();
|
|
||||||
} else {
|
|
||||||
msg.textContent = data.error || 'Hiba történt';
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = 'Frissítés telepítése';
|
|
||||||
if (checkBtn) checkBtn.disabled = false;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function() {
|
|
||||||
msg.textContent = 'Kapcsolódási hiba';
|
|
||||||
pollUntilBack();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function pollUntilBack() {
|
|
||||||
var iv = setInterval(function() {
|
|
||||||
fetch('/api/health')
|
|
||||||
.then(function(r) {
|
|
||||||
if (r.ok) {
|
|
||||||
clearInterval(iv);
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function() {});
|
|
||||||
}, 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
// If an update is already in flight when the page loads (e.g. a floor-driven AUTO-update kicked off by
|
|
||||||
// the hub, not a button click), surface the same restart-poll panel so the page recovers itself.
|
|
||||||
{{if .UpdateRunning}}
|
|
||||||
pollUntilBack();
|
|
||||||
{{end}}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Section: Storage Paths -->
|
<!-- Section: Storage Paths -->
|
||||||
<div class="settings-card">
|
<div class="settings-card">
|
||||||
<h3>Adattárolók</h3>
|
<h3>Adattárolók</h3>
|
||||||
@@ -372,6 +162,11 @@ pollUntilBack();
|
|||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
{{else}}
|
||||||
|
<div class="empty-state" style="padding:1.5rem">
|
||||||
|
Nincs regisztrált adattároló. Adjon hozzá egyet az alábbi űrlappal.
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
||||||
<!-- NAS network storage (Part A2) — a distinct class from the physical drives above. No
|
<!-- NAS network storage (Part A2) — a distinct class from the physical drives above. No
|
||||||
leválasztás/leszerelés/áthelyezés/törlés; the only lifecycle action is Eltávolítás. -->
|
leválasztás/leszerelés/áthelyezés/törlés; the only lifecycle action is Eltávolítás. -->
|
||||||
@@ -518,15 +313,10 @@ pollUntilBack();
|
|||||||
// storageMigrateAll) should fire the one-time post-completion reload.
|
// storageMigrateAll) should fire the one-time post-completion reload.
|
||||||
(function(){ fetch('/api/storage/migrate/status').then(function(r){return r.json();}).then(function(d){ var job=d.data&&d.data.job; if(job && job.phase!=='done' && job.phase!=='aborted'){ migWatch(); } }).catch(function(){}); })();
|
(function(){ fetch('/api/storage/migrate/status').then(function(r){return r.json();}).then(function(d){ var job=d.data&&d.data.job; if(job && job.phase!=='done' && job.phase!=='aborted'){ migWatch(); } }).catch(function(){}); })();
|
||||||
</script>
|
</script>
|
||||||
{{else}}
|
|
||||||
<div class="empty-state" style="padding:1.5rem">
|
|
||||||
Nincs regisztrált adattároló. Adjon hozzá egyet az alábbi űrlappal.
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
<div style="margin-top:1rem;display:flex;gap:.75rem;flex-wrap:wrap">
|
<div style="margin-top:1rem;display:flex;gap:.75rem;flex-wrap:wrap">
|
||||||
<a href="/settings/storage/init" class="btn btn-sm btn-outline">🔧 Új meghajtó inicializálása</a>
|
<a href="/storage/init" class="btn btn-sm btn-outline"><svg class="ico ico-sm"><use href="#i-plus"/></svg> Új meghajtó inicializálása</a>
|
||||||
<a href="/settings/storage/attach" class="btn btn-sm btn-outline">🔗 Meglévő meghajtó csatolása</a>
|
<a href="/storage/attach" class="btn btn-sm btn-outline"><svg class="ico ico-sm"><use href="#i-link"/></svg> Meglévő meghajtó csatolása</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="margin-top:1.5rem">
|
<div style="margin-top:1.5rem">
|
||||||
@@ -733,597 +523,7 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
|
|||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Section: Geo-Restriction -->
|
|
||||||
<div class="settings-card">
|
|
||||||
<h3>Földrajzi korlátozás</h3>
|
|
||||||
<p class="settings-card-desc">
|
|
||||||
Ország alapján korlátozható a webes alkalmazások elérése a Cloudflare WAF segítségével.
|
|
||||||
<br><span class="form-hint">A helyi hálózati hozzáférés mindig engedélyezett (nem halad át a Cloudflare-en).</span>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
{{if not .CFConfigured}}
|
|
||||||
<div class="alert alert-info">
|
|
||||||
A Cloudflare API token nincs konfigurálva. Kérd az üzemeltetőt a beállításhoz.<br>
|
|
||||||
<small>A tokennek <strong>Zone WAF:Edit</strong> jogosultsággal kell rendelkeznie.</small>
|
|
||||||
</div>
|
|
||||||
{{else}}
|
|
||||||
<div id="geo-status-msg"></div>
|
|
||||||
|
|
||||||
<label class="toggle" style="margin-bottom:1rem">
|
|
||||||
<input type="checkbox" id="geo-enabled" {{if .GeoEnabled}}checked{{end}}
|
|
||||||
onchange="toggleGeo(this.checked)">
|
|
||||||
<span class="toggle-label">Geo-korlátozás aktív</span>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div id="geo-details" {{if not .GeoEnabled}}style="display:none"{{end}}>
|
|
||||||
<!-- Global allowed countries -->
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Engedélyezett országok (globális)</label>
|
|
||||||
<div class="geo-country-selector" id="geo-countries">
|
|
||||||
<input type="text" id="geo-search" class="form-control"
|
|
||||||
placeholder="Ország keresése..."
|
|
||||||
autocomplete="off"
|
|
||||||
oninput="filterCountries(this.value)"
|
|
||||||
onfocus="showCountryList()"
|
|
||||||
onblur="setTimeout(function(){hideCountryList()},200)">
|
|
||||||
<div class="geo-country-list" id="geo-country-list"></div>
|
|
||||||
</div>
|
|
||||||
<div class="geo-selected-tags" id="geo-selected-tags"></div>
|
|
||||||
<span class="form-hint">Csak a kiválasztott országokból érhető el a rendszer.</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Per-app overrides -->
|
|
||||||
<div class="form-group" style="margin-top:1.5rem">
|
|
||||||
<label>Alkalmazás-specifikus felülírások</label>
|
|
||||||
<div id="geo-app-overrides"></div>
|
|
||||||
{{if .DeployedApps}}
|
|
||||||
<div style="margin-top:.5rem;display:flex;align-items:center;gap:.5rem">
|
|
||||||
<select id="geo-add-app-select" class="form-control" style="max-width:250px">
|
|
||||||
<option value="">— Alkalmazás kiválasztása —</option>
|
|
||||||
{{range .DeployedApps}}
|
|
||||||
<option value="{{.Name}}">{{.Display}}</option>
|
|
||||||
{{end}}
|
|
||||||
</select>
|
|
||||||
<button class="btn btn-sm btn-outline" onclick="addAppOverride()">+ Hozzáadás</button>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Sync status & save -->
|
|
||||||
<div class="form-group" style="margin-top:1.5rem">
|
|
||||||
<div style="display:flex;align-items:center;gap:1rem;flex-wrap:wrap">
|
|
||||||
<button class="btn btn-primary" id="btn-geo-save" onclick="saveGeoSettings()">
|
|
||||||
Mentés és szinkronizálás
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-outline" onclick="triggerGeoSync()">Kézi szinkronizálás</button>
|
|
||||||
<span id="geo-sync-status" class="form-hint">
|
|
||||||
{{if .GeoLastSync}}Utolsó szinkronizálás: {{.GeoLastSync}}{{end}}
|
|
||||||
{{if .GeoLastError}} <span class="state-text-crit">{{.GeoLastError}}</span>{{end}}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
(function(){
|
|
||||||
// Geo-restriction UI state
|
|
||||||
var allCountries = [];
|
|
||||||
var selectedCountries = {{json .GeoAllowedCountries}};
|
|
||||||
var appOverrides = {{json .GeoAppOverrides}};
|
|
||||||
|
|
||||||
// Load countries list on first use
|
|
||||||
function ensureCountries(cb) {
|
|
||||||
if (allCountries.length > 0) { cb(); return; }
|
|
||||||
fetch('/api/geo/countries', {headers: csrfHeaders()})
|
|
||||||
.then(function(r){return r.json()})
|
|
||||||
.then(function(d){
|
|
||||||
if (d.ok) allCountries = d.data;
|
|
||||||
cb();
|
|
||||||
})
|
|
||||||
.catch(function(){ cb(); });
|
|
||||||
}
|
|
||||||
|
|
||||||
window.toggleGeo = function(enabled) {
|
|
||||||
document.getElementById('geo-details').style.display = enabled ? '' : 'none';
|
|
||||||
if (enabled) ensureCountries(renderTags);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.showCountryList = function() {
|
|
||||||
ensureCountries(function(){ filterCountries(document.getElementById('geo-search').value); });
|
|
||||||
};
|
|
||||||
|
|
||||||
window.hideCountryList = function() {
|
|
||||||
document.getElementById('geo-country-list').style.display = 'none';
|
|
||||||
};
|
|
||||||
|
|
||||||
window.filterCountries = function(query) {
|
|
||||||
var list = document.getElementById('geo-country-list');
|
|
||||||
var q = query.toLowerCase();
|
|
||||||
var html = '';
|
|
||||||
var count = 0;
|
|
||||||
for (var i = 0; i < allCountries.length && count < 15; i++) {
|
|
||||||
var c = allCountries[i];
|
|
||||||
if (selectedCountries.indexOf(c.code) >= 0) continue;
|
|
||||||
if (q && c.name.toLowerCase().indexOf(q) < 0 && c.code.toLowerCase().indexOf(q) < 0) continue;
|
|
||||||
html += '<div class="geo-country-option" onmousedown="addCountry(\'' + c.code + '\',\'' + escHtml(c.name) + '\')">'
|
|
||||||
+ escHtml(c.name) + ' <small>(' + c.code + ')</small></div>';
|
|
||||||
count++;
|
|
||||||
}
|
|
||||||
list.innerHTML = html || '<div class="geo-country-option" style="opacity:.5">Nincs találat</div>';
|
|
||||||
// Reveal with 'block', NOT '' — the .geo-country-list CSS default is display:none,
|
|
||||||
// and clearing the inline style ('') would fall back to that and keep the (populated)
|
|
||||||
// list hidden. This was the country-autocomplete "no list" bug.
|
|
||||||
list.style.display = count > 0 || q ? 'block' : 'none';
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addCountry = function(code, name) {
|
|
||||||
if (selectedCountries.indexOf(code) >= 0) return;
|
|
||||||
selectedCountries.push(code);
|
|
||||||
renderTags();
|
|
||||||
document.getElementById('geo-search').value = '';
|
|
||||||
hideCountryList();
|
|
||||||
};
|
|
||||||
|
|
||||||
window.removeCountry = function(code) {
|
|
||||||
if (code === 'HU') {
|
|
||||||
if (!confirm('Figyelem: Magyarország eltávolítása azt jelenti, hogy magyar IP-ről sem lesz elérhető a rendszer távolról. Biztosan folytatja?')) return;
|
|
||||||
}
|
|
||||||
selectedCountries = selectedCountries.filter(function(c){return c !== code});
|
|
||||||
renderTags();
|
|
||||||
};
|
|
||||||
|
|
||||||
function renderTags() {
|
|
||||||
var el = document.getElementById('geo-selected-tags');
|
|
||||||
var html = '';
|
|
||||||
for (var i = 0; i < selectedCountries.length; i++) {
|
|
||||||
var code = selectedCountries[i];
|
|
||||||
var name = countryName(code);
|
|
||||||
var isHU = code === 'HU' ? ' geo-tag-hu' : '';
|
|
||||||
html += '<span class="geo-tag' + isHU + '">'
|
|
||||||
+ escHtml(name) + ' (' + code + ') '
|
|
||||||
+ '<span class="geo-tag-remove" onclick="removeCountry(\'' + code + '\')">×</span>'
|
|
||||||
+ '</span>';
|
|
||||||
}
|
|
||||||
el.innerHTML = html;
|
|
||||||
renderAppOverrides();
|
|
||||||
}
|
|
||||||
|
|
||||||
function countryName(code) {
|
|
||||||
for (var i = 0; i < allCountries.length; i++) {
|
|
||||||
if (allCountries[i].code === code) return allCountries[i].name;
|
|
||||||
}
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Per-app overrides ---
|
|
||||||
window.addAppOverride = function() {
|
|
||||||
var sel = document.getElementById('geo-add-app-select');
|
|
||||||
var appName = sel.value;
|
|
||||||
if (!appName) return;
|
|
||||||
if (!appOverrides) appOverrides = {};
|
|
||||||
if (appOverrides[appName]) { sel.value = ''; return; }
|
|
||||||
// Default: same countries as global
|
|
||||||
appOverrides[appName] = {allowed_countries: selectedCountries.slice()};
|
|
||||||
sel.value = '';
|
|
||||||
renderAppOverrides();
|
|
||||||
};
|
|
||||||
|
|
||||||
window.removeAppOverride = function(appName) {
|
|
||||||
delete appOverrides[appName];
|
|
||||||
renderAppOverrides();
|
|
||||||
};
|
|
||||||
|
|
||||||
window.toggleAppCountry = function(appName, code, el) {
|
|
||||||
var ov = appOverrides[appName];
|
|
||||||
if (!ov) return;
|
|
||||||
var idx = ov.allowed_countries.indexOf(code);
|
|
||||||
if (idx >= 0) {
|
|
||||||
if (code === 'HU' && !confirm('Magyarország eltávolítása nem ajánlott. Folytatja?')) {
|
|
||||||
el.checked = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
ov.allowed_countries.splice(idx, 1);
|
|
||||||
} else {
|
|
||||||
ov.allowed_countries.push(code);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function renderAppOverrides() {
|
|
||||||
var el = document.getElementById('geo-app-overrides');
|
|
||||||
if (!appOverrides || Object.keys(appOverrides).length === 0) {
|
|
||||||
el.innerHTML = '<p class="form-hint">Nincs alkalmazás-specifikus beállítás. Minden alkalmazás a globális beállítást követi.</p>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var html = '';
|
|
||||||
for (var appName in appOverrides) {
|
|
||||||
var ov = appOverrides[appName];
|
|
||||||
var displayName = appName;
|
|
||||||
// Try to find display name from select
|
|
||||||
var opts = document.getElementById('geo-add-app-select');
|
|
||||||
if (opts) {
|
|
||||||
for (var j = 0; j < opts.options.length; j++) {
|
|
||||||
if (opts.options[j].value === appName) { displayName = opts.options[j].text; break; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
html += '<div class="geo-app-override-row">';
|
|
||||||
html += '<strong>' + escHtml(displayName) + '</strong>';
|
|
||||||
html += '<div class="geo-selected-tags" style="flex:1;margin:0 .5rem">';
|
|
||||||
for (var i = 0; i < ov.allowed_countries.length; i++) {
|
|
||||||
var code = ov.allowed_countries[i];
|
|
||||||
html += '<span class="geo-tag geo-tag-sm">' + code + '</span>';
|
|
||||||
}
|
|
||||||
html += '</div>';
|
|
||||||
html += '<button class="btn btn-sm btn-outline" onclick="editAppOverride(\'' + appName + '\')">Szerkesztés</button>';
|
|
||||||
html += '<button class="btn btn-sm btn-danger-outline" onclick="removeAppOverride(\'' + appName + '\')">Törlés</button>';
|
|
||||||
html += '</div>';
|
|
||||||
}
|
|
||||||
el.innerHTML = html;
|
|
||||||
}
|
|
||||||
|
|
||||||
window.editAppOverride = function(appName) {
|
|
||||||
var ov = appOverrides[appName];
|
|
||||||
if (!ov) return;
|
|
||||||
ensureCountries(function(){
|
|
||||||
var checked = {};
|
|
||||||
for (var i = 0; i < ov.allowed_countries.length; i++) checked[ov.allowed_countries[i]] = true;
|
|
||||||
var html = '<div class="geo-edit-overlay" id="geo-edit-' + appName + '">';
|
|
||||||
html += '<h4>Engedélyezett országok: ' + escHtml(appName) + '</h4>';
|
|
||||||
html += '<div class="geo-edit-grid">';
|
|
||||||
for (var i = 0; i < allCountries.length; i++) {
|
|
||||||
var c = allCountries[i];
|
|
||||||
html += '<label class="geo-edit-item"><input type="checkbox" value="' + c.code + '"'
|
|
||||||
+ (checked[c.code] ? ' checked' : '') + ' onchange="toggleAppCountry(\'' + appName + '\',\'' + c.code + '\',this)">'
|
|
||||||
+ ' ' + escHtml(c.name) + ' (' + c.code + ')</label>';
|
|
||||||
}
|
|
||||||
html += '</div>';
|
|
||||||
html += '<button class="btn btn-sm btn-primary" style="margin-top:.5rem" onclick="closeAppEdit(\'' + appName + '\')">Kész</button>';
|
|
||||||
html += '</div>';
|
|
||||||
document.getElementById('geo-app-overrides').innerHTML += html;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
window.closeAppEdit = function(appName) {
|
|
||||||
var el = document.getElementById('geo-edit-' + appName);
|
|
||||||
if (el) el.remove();
|
|
||||||
renderAppOverrides();
|
|
||||||
};
|
|
||||||
|
|
||||||
// --- Save & Sync ---
|
|
||||||
window.saveGeoSettings = function() {
|
|
||||||
var btn = document.getElementById('btn-geo-save');
|
|
||||||
var status = document.getElementById('geo-status-msg');
|
|
||||||
btn.disabled = true;
|
|
||||||
btn.textContent = 'Mentés...';
|
|
||||||
|
|
||||||
var payload = {
|
|
||||||
enabled: document.getElementById('geo-enabled').checked,
|
|
||||||
allowed_countries: selectedCountries
|
|
||||||
};
|
|
||||||
|
|
||||||
fetch('/api/geo/settings', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
})
|
|
||||||
.then(function(r){return r.json()})
|
|
||||||
.then(function(d){
|
|
||||||
if (d.ok) {
|
|
||||||
status.innerHTML = '<div class="alert alert-info">' + (d.message || 'Mentve') + '</div>';
|
|
||||||
// Save per-app overrides
|
|
||||||
if (appOverrides && Object.keys(appOverrides).length > 0) {
|
|
||||||
saveAllAppOverrides();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
status.innerHTML = '<div class="alert alert-error">' + (d.error || 'Hiba') + '</div>';
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(function(err){
|
|
||||||
status.innerHTML = '<div class="alert alert-error">Hálózati hiba</div>';
|
|
||||||
})
|
|
||||||
.finally(function(){
|
|
||||||
btn.disabled = false;
|
|
||||||
btn.textContent = 'Mentés és szinkronizálás';
|
|
||||||
setTimeout(function(){ status.innerHTML = ''; }, 8000);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function saveAllAppOverrides() {
|
|
||||||
for (var appName in appOverrides) {
|
|
||||||
(function(name, ov){
|
|
||||||
fetch('/api/stacks/' + name + '/geo/override', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: Object.assign({'Content-Type': 'application/json'}, csrfHeaders()),
|
|
||||||
body: JSON.stringify({allowed_countries: ov.allowed_countries})
|
|
||||||
});
|
|
||||||
})(appName, appOverrides[appName]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
window.triggerGeoSync = function() {
|
|
||||||
fetch('/api/geo/sync', {method:'POST', headers: csrfHeaders()})
|
|
||||||
.then(function(r){return r.json()})
|
|
||||||
.then(function(d){
|
|
||||||
var status = document.getElementById('geo-sync-status');
|
|
||||||
status.textContent = d.ok ? 'Szinkronizálás elindítva...' : (d.error || 'Hiba');
|
|
||||||
setTimeout(function(){
|
|
||||||
fetch('/api/geo/status', {headers: csrfHeaders()})
|
|
||||||
.then(function(r){return r.json()})
|
|
||||||
.then(function(d){
|
|
||||||
if (d.ok && d.data) {
|
|
||||||
var sync = d.data.last_sync || '';
|
|
||||||
var err = d.data.last_sync_error || '';
|
|
||||||
status.innerHTML = sync ? ('Utolsó: ' + sync.substring(0,19).replace('T',' ')) : '';
|
|
||||||
if (err) status.innerHTML += ' <span class="state-text-crit">' + escHtml(err) + '</span>';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, 5000);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
function escHtml(s) {
|
|
||||||
var d = document.createElement('div');
|
|
||||||
d.textContent = s;
|
|
||||||
return d.innerHTML;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize on load
|
|
||||||
if (document.getElementById('geo-enabled') && document.getElementById('geo-enabled').checked) {
|
|
||||||
ensureCountries(renderTags);
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!-- Section B: Password Change -->
|
|
||||||
<div class="settings-card">
|
|
||||||
<h3>Jelszó módosítás</h3>
|
|
||||||
{{if .AuthEnabled}}
|
|
||||||
{{if .PasswordError}}<div class="alert alert-error">{{.PasswordError}}</div>{{end}}
|
|
||||||
<form method="POST" action="/settings/password">
|
|
||||||
{{.CSRFField}}
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="current_password">Jelenlegi jelszó</label>
|
|
||||||
<input type="password" id="current_password" name="current_password" required
|
|
||||||
placeholder="Adja meg a jelenlegi jelszavát" class="form-control">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="new_password">Új jelszó</label>
|
|
||||||
<input type="password" id="new_password" name="new_password" required minlength="8"
|
|
||||||
placeholder="Legalább 8 karakter" class="form-control">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="confirm_password">Új jelszó megerősítése</label>
|
|
||||||
<input type="password" id="confirm_password" name="confirm_password" required minlength="8"
|
|
||||||
placeholder="Jelszó mégegyszer" class="form-control">
|
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-primary">Jelszó módosítása</button>
|
|
||||||
</form>
|
|
||||||
{{else}}
|
|
||||||
<div class="alert alert-info">
|
|
||||||
A jelszavas védelem nincs beállítva. Kérd az üzemeltetőt a beállításhoz.
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Section C: Notification Preferences -->
|
|
||||||
<div class="settings-card">
|
|
||||||
<h3>Értesítések</h3>
|
|
||||||
{{if .HubEnabled}}
|
|
||||||
{{if .NotificationSuccess}}<div class="alert alert-info">{{.NotificationSuccess}}</div>{{end}}
|
|
||||||
{{if .NotificationError}}<div class="alert alert-error">{{.NotificationError}}</div>{{end}}
|
|
||||||
<form method="POST" action="/settings/notifications">
|
|
||||||
{{.CSRFField}}
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="notification_email">E-mail cím</label>
|
|
||||||
<input type="email" id="notification_email" name="notification_email"
|
|
||||||
value="{{with .NotificationPrefs}}{{.Email}}{{end}}"
|
|
||||||
placeholder="pelda@email.hu" class="form-control">
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Hibák és figyelmeztetések:</label>
|
|
||||||
<div class="checkbox-group">
|
|
||||||
<label class="toggle">
|
|
||||||
<input type="checkbox" name="event_backup_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "backup_failed"}}checked{{end}}{{end}}{{end}}>
|
|
||||||
<span class="toggle-label">Biztonsági mentés sikertelen</span>
|
|
||||||
</label>
|
|
||||||
<label class="toggle">
|
|
||||||
<input type="checkbox" name="event_db_dump_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "db_dump_failed"}}checked{{end}}{{end}}{{end}}>
|
|
||||||
<span class="toggle-label">Adatbázis mentés sikertelen</span>
|
|
||||||
</label>
|
|
||||||
<label class="toggle">
|
|
||||||
<input type="checkbox" name="event_backup_integrity_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "backup_integrity_failed"}}checked{{end}}{{end}}{{end}}>
|
|
||||||
<span class="toggle-label">Mentés sérülés észlelve</span>
|
|
||||||
</label>
|
|
||||||
<label class="toggle">
|
|
||||||
<input type="checkbox" name="event_crossdrive_failed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "crossdrive_failed"}}checked{{end}}{{end}}{{end}}>
|
|
||||||
<span class="toggle-label">Másodlagos mentés sikertelen</span>
|
|
||||||
</label>
|
|
||||||
<label class="toggle">
|
|
||||||
<input type="checkbox" name="event_disk_alerts" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "disk_warning"}}checked{{end}}{{end}}{{end}}>
|
|
||||||
<span class="toggle-label">Lemez figyelmeztetés (90%+)</span>
|
|
||||||
</label>
|
|
||||||
<label class="toggle">
|
|
||||||
<input type="checkbox" name="event_storage_disconnected" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "storage_disconnected"}}checked{{end}}{{end}}{{end}}>
|
|
||||||
<span class="toggle-label">Meghajtó leválasztva</span>
|
|
||||||
</label>
|
|
||||||
<label class="toggle">
|
|
||||||
<input type="checkbox" name="event_node_down" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "node_down"}}checked{{end}}{{end}}{{end}}>
|
|
||||||
<span class="toggle-label">Szerver nem elérhető</span>
|
|
||||||
</label>
|
|
||||||
<label class="toggle">
|
|
||||||
<input type="checkbox" name="event_health_critical" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "health_critical"}}checked{{end}}{{end}}{{end}}>
|
|
||||||
<span class="toggle-label">Rendszer állapot kritikus</span>
|
|
||||||
</label>
|
|
||||||
<label class="toggle">
|
|
||||||
<input type="checkbox" name="event_expected_missed" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "expected_backup_missed"}}checked{{end}}{{end}}{{end}}>
|
|
||||||
<span class="toggle-label">Elvárt mentés elmaradt</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>Tájékoztató:</label>
|
|
||||||
<div class="checkbox-group">
|
|
||||||
<label class="toggle">
|
|
||||||
<input type="checkbox" name="event_storage_reconnected" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "storage_reconnected"}}checked{{end}}{{end}}{{end}}>
|
|
||||||
<span class="toggle-label">Meghajtó újra csatlakoztatva</span>
|
|
||||||
</label>
|
|
||||||
<label class="toggle">
|
|
||||||
<input type="checkbox" name="event_health_recovered" {{with .NotificationPrefs}}{{range .EnabledEvents}}{{if eq . "health_recovered"}}checked{{end}}{{end}}{{end}}>
|
|
||||||
<span class="toggle-label">Rendszer állapot helyreállt</span>
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="cooldown_hours">Értesítési szünet</label>
|
|
||||||
<div class="form-inline">
|
|
||||||
<input type="number" id="cooldown_hours" name="cooldown_hours" min="1" max="168"
|
|
||||||
value="{{with .NotificationPrefs}}{{.CooldownHours}}{{end}}"
|
|
||||||
class="form-control form-control-narrow">
|
|
||||||
<span class="form-hint">óra (azonos probléma esetén ennyi ideig nem küld újat)</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-actions">
|
|
||||||
<button type="submit" class="btn btn-primary">Mentés</button>
|
|
||||||
<button type="submit" formaction="/settings/notifications/test" class="btn btn-outline">Teszt email küldése</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
{{else}}
|
|
||||||
<div class="alert alert-info">
|
|
||||||
Az értesítések a központi rendszeren keresztül működnek, ami jelenleg nincs bekapcsolva.
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Section: App email (SMTP relay) -->
|
|
||||||
{{if .AppEmailAvailable}}
|
|
||||||
<div class="settings-card">
|
|
||||||
<h3>Alkalmazás-email</h3>
|
|
||||||
<p class="settings-card-desc">
|
|
||||||
Az alkalmazások a Felhom-on keresztül küldhetnek emailt (pl. jelszó-visszaállítás, meghívók),
|
|
||||||
külön email-szolgáltató beállítása nélkül. A feladó címe minden alkalmazásnál a saját
|
|
||||||
<em><alkalmazás>@felhom.eu</em> címe lesz.
|
|
||||||
</p>
|
|
||||||
{{if .AppEmailSuccess}}<div class="alert alert-info">{{.AppEmailSuccess}}</div>{{end}}
|
|
||||||
{{if .AppEmailError}}<div class="alert alert-error">{{.AppEmailError}}</div>{{end}}
|
|
||||||
<form method="POST" action="/settings/app-email">
|
|
||||||
{{.CSRFField}}
|
|
||||||
<div class="form-group">
|
|
||||||
<label style="display:flex;align-items:center;gap:.5rem">
|
|
||||||
<input type="checkbox" name="app_email_enabled" value="on" {{if .AppEmailEnabled}}checked{{end}}>
|
|
||||||
Alkalmazás-email engedélyezése
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="app_email_from_name">Feladó neve (opcionális)</label>
|
|
||||||
<input type="text" id="app_email_from_name" name="app_email_from_name"
|
|
||||||
value="{{.AppEmailFromName}}" placeholder="pl. a háztartás neve" class="form-control">
|
|
||||||
<span class="form-hint">Ez jelenik meg a kimenő emailek feladójaként az alkalmazás neve mellett.</span>
|
|
||||||
</div>
|
|
||||||
<div class="form-actions">
|
|
||||||
<button type="submit" class="btn btn-primary">Mentés</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
<p class="settings-card-desc" style="margin-top:.75rem">
|
|
||||||
Bekapcsolás után az egyes alkalmazásoknál is engedélyezni kell az email-küldést (az alkalmazás oldalán).
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
<!-- Section: Recovery Info -->
|
|
||||||
{{if .RetrievalPassword}}
|
|
||||||
<div class="settings-card">
|
|
||||||
<h3>Veszhelyzeti informaciok</h3>
|
|
||||||
<p class="settings-card-desc">
|
|
||||||
Ezeket az adatokat mentse el biztos helyre. Ujratelepites eseten szukseg lesz rajuk a rendszer visszaallitasahoz.
|
|
||||||
</p>
|
|
||||||
<div class="settings-grid">
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Ugyfel azonosito</span>
|
|
||||||
<span class="settings-value mono">{{.CustomerID}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Hub URL</span>
|
|
||||||
<span class="settings-value mono">{{.HubURL}}</span>
|
|
||||||
</div>
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Visszaallitasi jelszo</span>
|
|
||||||
<span class="settings-value">
|
|
||||||
<span id="retrieval-pw-hidden">••••••••••••••••
|
|
||||||
<button type="button" class="btn btn-xs btn-outline" onclick="document.getElementById('retrieval-pw-hidden').style.display='none';document.getElementById('retrieval-pw-visible').style.display='inline';">Megjelenit</button>
|
|
||||||
</span>
|
|
||||||
<span id="retrieval-pw-visible" style="display:none">
|
|
||||||
<code class="mono">{{.RetrievalPassword}}</code>
|
|
||||||
<button type="button" class="btn btn-xs btn-outline" onclick="document.getElementById('retrieval-pw-visible').style.display='none';document.getElementById('retrieval-pw-hidden').style.display='inline';">Elrejt</button>
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="settings-row">
|
|
||||||
<span class="settings-label">Tamogatas</span>
|
|
||||||
<span class="settings-value">
|
|
||||||
<a href="mailto:{{.SupportEmail}}" style="color: var(--accent-blue, #0088cc);">{{.SupportEmail}}</a>
|
|
||||||
|
|
|
||||||
<a href="{{.SupportURL}}" target="_blank" style="color: var(--accent-blue, #0088cc);">felhom.eu/kapcsolat</a>
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{{end}}
|
|
||||||
|
|
||||||
<!-- Section: Controller restart (self-serve) — always available (not gated on RetrievalPassword) -->
|
|
||||||
<div class="settings-card">
|
|
||||||
<h3>Vezérlő újraindítása</h3>
|
|
||||||
<p class="settings-card-desc">
|
|
||||||
Ha a vezérlő hibásan működik, itt biztonságosan újraindíthatja — nem kell az egész szervert újraindítani.
|
|
||||||
Az alkalmazásai futnak tovább; csak a vezérlő indul újra (néhány másodperc).
|
|
||||||
</p>
|
|
||||||
<div id="restart-status"></div>
|
|
||||||
<button type="button" class="btn btn-outline" id="btn-restart-controller" onclick="restartController()">Vezérlő újraindítása</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Section: Full server (guest) restart — a deliberate maintenance affordance, sibling to the controller restart -->
|
|
||||||
<div class="settings-card">
|
|
||||||
<h3>Kiszolgáló újraindítása</h3>
|
|
||||||
<p class="settings-card-desc">
|
|
||||||
Az egész kiszolgáló (szerver) újraindítása. Minden alkalmazás rövid időre leáll, és a vezérlőpult kb. 30 másodpercig nem elérhető. Akkor használja, ha a teljes rendszer újraindítására van szükség — egyébként a fenti „Vezérlő újraindítása” elegendő.
|
|
||||||
</p>
|
|
||||||
<div id="server-restart-status"></div>
|
|
||||||
<button type="button" class="btn btn-outline" id="btn-restart-server" onclick="restartServer()">Kiszolgáló újraindítása</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function restartController() {
|
|
||||||
if (!confirm('Biztosan újraindítja a vezérlőt? A művelet néhány másodpercig tart, és a felület rövid időre elérhetetlen lesz.')) return;
|
|
||||||
var btn = document.getElementById('btn-restart-controller');
|
|
||||||
var status = document.getElementById('restart-status');
|
|
||||||
btn.disabled = true;
|
|
||||||
status.innerHTML = '<div class="alert alert-info">Újraindítás folyamatban… újracsatlakozás…</div>';
|
|
||||||
fetch('/api/selfrestart', { method: 'POST', headers: csrfHeaders() })
|
|
||||||
.then(function(){ pollRestart(0); })
|
|
||||||
.catch(function(){ pollRestart(0); }); // connection may drop as the process exits — poll regardless
|
|
||||||
}
|
|
||||||
function restartServer() {
|
|
||||||
if (!confirm('Biztosan újraindítja a kiszolgálót? Az alkalmazások és a vezérlőpult kb. 30 másodpercre elérhetetlenné válnak.')) return;
|
|
||||||
var btn = document.getElementById('btn-restart-server');
|
|
||||||
var status = document.getElementById('server-restart-status');
|
|
||||||
if (btn) btn.disabled = true;
|
|
||||||
if (status) status.innerHTML = '<div class="alert alert-info">Újraindítás folyamatban… a vezérlőpult néhány másodperc múlva újratölt.</div>';
|
|
||||||
fetch('/api/server/reboot', { method: 'POST', headers: csrfHeaders() })
|
|
||||||
.then(function(){ pollRestart(0); })
|
|
||||||
.catch(function(){ pollRestart(0); }); // the guest reboot drops the connection — poll regardless
|
|
||||||
}
|
|
||||||
function pollRestart(attempt) {
|
|
||||||
if (attempt > 60) { // ~2 min cap — never leave the user on a dead page silently
|
|
||||||
document.getElementById('restart-status').innerHTML =
|
|
||||||
'<div class="alert alert-error">Az újraindítás a vártnál tovább tart. Töltse újra az oldalt kézzel.</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
setTimeout(function(){
|
|
||||||
fetch('/', { method: 'GET', cache: 'no-store' })
|
|
||||||
.then(function(r){ if (r.ok) { window.location.reload(); } else { pollRestart(attempt + 1); } })
|
|
||||||
.catch(function(){ pollRestart(attempt + 1); });
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
function editStorageLabel(path, currentLabel) {
|
function editStorageLabel(path, currentLabel) {
|
||||||
var wrap = document.getElementById('label-wrap-' + path);
|
var wrap = document.getElementById('label-wrap-' + path);
|
||||||
if (!wrap) return;
|
if (!wrap) return;
|
||||||
@@ -1976,26 +1976,6 @@ a.stat-card:hover {
|
|||||||
margin-top: auto;
|
margin-top: auto;
|
||||||
border-top: 1px solid var(--line);
|
border-top: 1px solid var(--line);
|
||||||
}
|
}
|
||||||
.sidebar-settings-link {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: .6rem;
|
|
||||||
padding: .75rem 1.5rem;
|
|
||||||
color: var(--text-2);
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: .95rem;
|
|
||||||
font-weight: 500;
|
|
||||||
transition: color 0.2s ease, background 0.2s ease;
|
|
||||||
}
|
|
||||||
.sidebar-settings-link:hover {
|
|
||||||
color: var(--blue-bright);
|
|
||||||
background: var(--blue-dim);
|
|
||||||
}
|
|
||||||
.sidebar-settings-link.active {
|
|
||||||
color: var(--blue-bright);
|
|
||||||
background: var(--blue-dim);
|
|
||||||
border-left: 3px solid var(--blue);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* --- Backup page: Storage overview grid --- */
|
/* --- Backup page: Storage overview grid --- */
|
||||||
.storage-overview-grid {
|
.storage-overview-grid {
|
||||||
@@ -3244,3 +3224,21 @@ span.mono, .mono { font-family: var(--font-data); }
|
|||||||
.login-title .title-accent {
|
.login-title .title-accent {
|
||||||
color: var(--blue-bright);
|
color: var(--blue-bright);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Sidebar settings group (D1) — group label + indented sub-links. */
|
||||||
|
.nav-group-label {
|
||||||
|
padding: .75rem 1.5rem .25rem;
|
||||||
|
font-size: .68rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: .1em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--text-3);
|
||||||
|
}
|
||||||
|
.nav-links-sub {
|
||||||
|
padding: 0 0 .5rem;
|
||||||
|
flex: none;
|
||||||
|
}
|
||||||
|
.nav-links-sub a {
|
||||||
|
padding: .5rem 1.5rem .5rem 1.75rem;
|
||||||
|
font-size: .88rem;
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,91 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""D1 §10 JS element-ID integrity gate.
|
||||||
|
|
||||||
|
For every template under internal/web/templates and internal/setup/templates, extract each
|
||||||
|
getElementById('X') / querySelector('#X') literal used by the file's inline JS and assert an
|
||||||
|
id="X" exists in the SAME file — or that the ID is created by that file's own JS (innerHTML /
|
||||||
|
insertAdjacentHTML string containing id="X" / id='X'), or is explicitly allowlisted below with
|
||||||
|
a justification.
|
||||||
|
|
||||||
|
Exit 1 on any unresolved reference. Run from the repo's controller/ directory:
|
||||||
|
python scripts/template_id_gate.py
|
||||||
|
"""
|
||||||
|
import io, os, re, sys
|
||||||
|
|
||||||
|
ROOTS = [
|
||||||
|
os.path.join("internal", "web", "templates"),
|
||||||
|
os.path.join("internal", "setup", "templates"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Dynamic-ID exceptions: (template, id-prefix-or-name) -> justification.
|
||||||
|
# Suffix-parameterized IDs (id + variable) are handled generically below; these are the rest.
|
||||||
|
ALLOW = {
|
||||||
|
# layout.html builds the alert/delete/remove modals entirely in JS and later looks them up.
|
||||||
|
("layout.html", "alert-modal"): "created by showAlert() via innerHTML in the same file",
|
||||||
|
("layout.html", "delete-modal"): "created by deleteOrphanStack() via innerHTML",
|
||||||
|
("layout.html", "remove-modal"): "created by removeStack() via innerHTML",
|
||||||
|
("layout.html", "confirm-delete-btn"): "created inside the delete-modal innerHTML",
|
||||||
|
("layout.html", "confirm-remove-btn"): "created inside the remove-modal innerHTML",
|
||||||
|
("layout.html", "delete-hdd-check"): "created inside the delete-modal innerHTML",
|
||||||
|
("layout.html", "remove-hdd-check"): "created inside the remove-modal innerHTML",
|
||||||
|
("layout.html", "remove-backup-check"): "created inside the remove-modal innerHTML",
|
||||||
|
("layout.html", "remove-hdd-keep-warning"): "created inside the remove-modal innerHTML",
|
||||||
|
("layout.html", "sync-btn"): "lives on stacks.html; syncTemplates() is shared layout JS guarded by if(!btn)return",
|
||||||
|
("layout.html", "sync-toast"): "lives on stacks.html; guarded null-check",
|
||||||
|
}
|
||||||
|
|
||||||
|
GET_RE = re.compile(r"getElementById\(\s*['\"]([A-Za-z0-9_-]+)['\"]\s*\)")
|
||||||
|
GET_DYN_RE = re.compile(r"getElementById\(\s*['\"]([A-Za-z0-9_-]+)['\"]\s*\+")
|
||||||
|
QS_RE = re.compile(r"querySelector\(\s*['\"]#([A-Za-z0-9_-]+)['\"]\s*\)")
|
||||||
|
ID_ATTR_RE = re.compile(r"""id=["']([A-Za-z0-9_{}\. $-]+)["']""")
|
||||||
|
ID_IN_JS_RE = re.compile(r"""id=\\?["']([A-Za-z0-9_-]+)\\?["']""")
|
||||||
|
|
||||||
|
|
||||||
|
def check(path):
|
||||||
|
fname = os.path.basename(path)
|
||||||
|
src = io.open(path, encoding="utf-8").read()
|
||||||
|
static_refs = set(GET_RE.findall(src)) | set(QS_RE.findall(src))
|
||||||
|
dyn_prefixes = set(GET_DYN_RE.findall(src))
|
||||||
|
# static refs regex also matches the dynamic form's literal — subtract prefixes used with '+'
|
||||||
|
static_refs -= dyn_prefixes
|
||||||
|
defined = set(ID_ATTR_RE.findall(src)) | set(ID_IN_JS_RE.findall(src))
|
||||||
|
defined_prefixes = tuple(d.split("{{")[0] for d in defined if "{{" in d or d.endswith("-"))
|
||||||
|
|
||||||
|
problems = []
|
||||||
|
for ref in sorted(static_refs):
|
||||||
|
if ref in defined:
|
||||||
|
continue
|
||||||
|
# a template-parameterized id like id="field-{{.EnvVar}}" legitimately renders
|
||||||
|
# ids such as field-SUBDOMAIN — match static refs against those prefixes
|
||||||
|
if defined_prefixes and ref.startswith(defined_prefixes):
|
||||||
|
continue
|
||||||
|
if (fname, ref) in ALLOW:
|
||||||
|
continue
|
||||||
|
problems.append("static #%s not defined in %s" % (ref, fname))
|
||||||
|
for pref in sorted(dyn_prefixes):
|
||||||
|
# a dynamic lookup 'x-' + var needs SOME id starting with that prefix (template- or JS-created)
|
||||||
|
if any(d.startswith(pref) for d in defined) or pref in defined_prefixes:
|
||||||
|
continue
|
||||||
|
if (fname, pref) in ALLOW:
|
||||||
|
continue
|
||||||
|
problems.append("dynamic prefix #%s* not defined in %s" % (pref, fname))
|
||||||
|
return problems
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
bad = []
|
||||||
|
for root in ROOTS:
|
||||||
|
for fn in sorted(os.listdir(root)):
|
||||||
|
if not fn.endswith(".html"):
|
||||||
|
continue
|
||||||
|
bad += check(os.path.join(root, fn))
|
||||||
|
if bad:
|
||||||
|
print("INTEGRITY GATE FAILED (%d):" % len(bad))
|
||||||
|
for b in bad:
|
||||||
|
print(" -", b)
|
||||||
|
sys.exit(1)
|
||||||
|
print("integrity gate OK — every JS element-ID reference resolves within its own template")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user