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
|
||||
}
|
||||
|
||||
// settingsData merges every subpage builder — legacy glue kept ONLY while the monolithic
|
||||
// settings.html exists (deleted with the D1 Part 2 template split).
|
||||
func (s *Server) settingsData() map[string]interface{} {
|
||||
data := s.systemPageData()
|
||||
for _, m := range []map[string]interface{}{s.storagePageData(), s.notificationsPageData(), s.securityPageData()} {
|
||||
for k, v := range m {
|
||||
if k == "Page" || k == "Title" {
|
||||
continue
|
||||
}
|
||||
data[k] = v
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
func (s *Server) settingsHandler(w http.ResponseWriter, r *http.Request) {
|
||||
s.executeTemplate(w, r, "settings", s.settingsData())
|
||||
s.executeTemplate(w, r, "settings_system", s.systemPageData())
|
||||
}
|
||||
|
||||
// storagePageHandler serves the Tárhely main-nav page (D1). Storage action flashes land here.
|
||||
func (s *Server) storagePageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data := s.settingsData()
|
||||
data := s.storagePageData()
|
||||
if msg := r.URL.Query().Get("storage_msg"); msg == "success" {
|
||||
data["StorageSuccess"] = r.URL.Query().Get("storage_detail")
|
||||
}
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
}
|
||||
|
||||
// settingsNotificationsPageHandler serves GET /settings/notifications (the POST on the same
|
||||
// path is the save handler — dispatch is split in the router).
|
||||
func (s *Server) settingsNotificationsPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
s.executeTemplate(w, r, "settings", s.settingsData())
|
||||
s.executeTemplate(w, r, "settings_notifications", s.notificationsPageData())
|
||||
}
|
||||
|
||||
// settingsSecurityPageHandler serves GET /settings/security.
|
||||
func (s *Server) settingsSecurityPageHandler(w http.ResponseWriter, r *http.Request) {
|
||||
s.executeTemplate(w, r, "settings", s.settingsData())
|
||||
s.executeTemplate(w, r, "settings_security", s.securityPageData())
|
||||
}
|
||||
|
||||
func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -1097,7 +1082,7 @@ func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request)
|
||||
s.logger.Printf("[DEBUG] [web] settingsPasswordHandler: password change attempt from %s", r.RemoteAddr)
|
||||
}
|
||||
|
||||
data := s.settingsData()
|
||||
data := s.securityPageData()
|
||||
|
||||
// Validate current password
|
||||
effectiveHash := s.effectivePasswordHash()
|
||||
@@ -1106,21 +1091,21 @@ func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request)
|
||||
s.logger.Printf("[DEBUG] [web] settingsPasswordHandler: current password mismatch from %s", r.RemoteAddr)
|
||||
}
|
||||
data["PasswordError"] = "Hibás jelenlegi jelszó"
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "settings_security", data)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate new password length
|
||||
if len(newPassword) < 8 {
|
||||
data["PasswordError"] = "A jelszónak legalább 8 karakter hosszúnak kell lennie"
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "settings_security", data)
|
||||
return
|
||||
}
|
||||
|
||||
// Validate passwords match
|
||||
if newPassword != confirmPassword {
|
||||
data["PasswordError"] = "A két jelszó nem egyezik"
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "settings_security", data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1129,7 +1114,7 @@ func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request)
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] [web] Failed to hash new password: %v", err)
|
||||
data["PasswordError"] = "Belső hiba a jelszó mentésekor"
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "settings_security", data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1137,7 +1122,7 @@ func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request)
|
||||
if err := s.settings.SetPasswordHash(string(hash)); err != nil {
|
||||
s.logger.Printf("[ERROR] [web] Failed to save password to settings.json: %v", err)
|
||||
data["PasswordError"] = "Belső hiba a jelszó mentésekor"
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "settings_security", data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1202,16 +1187,16 @@ func (s *Server) settingsNotificationsHandler(w http.ResponseWriter, r *http.Req
|
||||
|
||||
if err := s.settings.SetNotificationPrefs(prefs); err != nil {
|
||||
s.logger.Printf("[ERROR] [web] Failed to save notification prefs: %v", err)
|
||||
data := s.settingsData()
|
||||
data := s.notificationsPageData()
|
||||
data["NotificationError"] = "Hiba a beállítások mentésekor"
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "settings_notifications", data)
|
||||
return
|
||||
}
|
||||
|
||||
s.logger.Printf("[INFO] [web] Notification preferences updated: email=%s, events=%v", email, enabledEvents)
|
||||
|
||||
// Sync preferences to hub
|
||||
data := s.settingsData()
|
||||
data := s.notificationsPageData()
|
||||
if s.notifier != nil && s.notifier.IsEnabled() {
|
||||
if err := s.notifier.SyncPreferences(email, enabledEvents, cooldownHours); err != nil {
|
||||
s.logger.Printf("[WARN] [web] Failed to sync preferences to hub: %v", err)
|
||||
@@ -1222,7 +1207,7 @@ func (s *Server) settingsNotificationsHandler(w http.ResponseWriter, r *http.Req
|
||||
} else {
|
||||
data["NotificationSuccess"] = "Értesítési beállítások mentve."
|
||||
}
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "settings_notifications", data)
|
||||
}
|
||||
|
||||
// settingsAppEmailHandler saves the global app-email toggle and starts/stops the on-box
|
||||
@@ -1232,39 +1217,39 @@ func (s *Server) settingsAppEmailHandler(w http.ResponseWriter, r *http.Request)
|
||||
enabled := r.FormValue("app_email_enabled") == "on" || r.FormValue("app_email_enabled") == "true"
|
||||
fromName := strings.TrimSpace(r.FormValue("app_email_from_name"))
|
||||
|
||||
data := s.settingsData()
|
||||
data := s.notificationsPageData()
|
||||
if err := s.settings.SetAppEmail(enabled, fromName); err != nil {
|
||||
s.logger.Printf("[ERROR] [web] Failed to save app-email toggle: %v", err)
|
||||
data["AppEmailError"] = "Hiba az alkalmazás-email beállítás mentésekor"
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "settings_notifications", data)
|
||||
return
|
||||
}
|
||||
// Reconcile the shim's running state with the new toggle.
|
||||
if s.mailShim != nil {
|
||||
if err := s.mailShim.Apply(enabled); err != nil {
|
||||
s.logger.Printf("[ERROR] [web] app-email shim could not be %s: %v", map[bool]string{true: "started", false: "stopped"}[enabled], err)
|
||||
data = s.settingsData()
|
||||
data = s.notificationsPageData()
|
||||
data["AppEmailError"] = "A beállítás elmentve, de az email-szolgáltatás indítása nem sikerült."
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "settings_notifications", data)
|
||||
return
|
||||
}
|
||||
}
|
||||
s.logger.Printf("[INFO] [web] App-email globally %s (from_name=%q)", map[bool]string{true: "enabled", false: "disabled"}[enabled], fromName)
|
||||
data = s.settingsData()
|
||||
data = s.notificationsPageData()
|
||||
if enabled {
|
||||
data["AppEmailSuccess"] = "Alkalmazás-email bekapcsolva. Kapcsold be az egyes alkalmazásoknál is, ahol email-küldést szeretnél."
|
||||
} else {
|
||||
data["AppEmailSuccess"] = "Alkalmazás-email kikapcsolva."
|
||||
}
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "settings_notifications", data)
|
||||
}
|
||||
|
||||
func (s *Server) settingsNotificationsTestHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data := s.settingsData()
|
||||
data := s.notificationsPageData()
|
||||
|
||||
if s.notifier == nil {
|
||||
data["NotificationError"] = "Az értesítések nincsenek bekapcsolva"
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "settings_notifications", data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1272,12 +1257,12 @@ func (s *Server) settingsNotificationsTestHandler(w http.ResponseWriter, r *http
|
||||
if err != nil {
|
||||
s.logger.Printf("[ERROR] [web] Test notification failed: %v", err)
|
||||
data["NotificationError"] = fmt.Sprintf("Teszt email küldése sikertelen: %v", err)
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "settings_notifications", data)
|
||||
return
|
||||
}
|
||||
|
||||
data["NotificationSuccess"] = "Teszt email elküldve."
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "settings_notifications", data)
|
||||
}
|
||||
|
||||
// --- Storage path management handlers ---
|
||||
@@ -1496,27 +1481,27 @@ func (s *Server) settingsStorageAddHandler(w http.ResponseWriter, r *http.Reques
|
||||
label = settings.InferStorageLabel(path)
|
||||
}
|
||||
|
||||
data := s.settingsData()
|
||||
data := s.storagePageData()
|
||||
|
||||
// 1. Exists and is directory
|
||||
fi, err := os.Stat(path)
|
||||
if err != nil || !fi.IsDir() {
|
||||
data["StorageError"] = "Az útvonal nem létezik vagy nem mappa."
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
|
||||
// 2. Is mount point
|
||||
if !system.IsMountPoint(path) {
|
||||
data["StorageError"] = "Ez az útvonal nem külön csatlakoztatott meghajtó. Adatok az SSD-re kerülnének!"
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
|
||||
// 3. Writable
|
||||
if !system.IsWritable(path) {
|
||||
data["StorageError"] = "Az útvonal nem írható."
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1524,7 +1509,7 @@ func (s *Server) settingsStorageAddHandler(w http.ResponseWriter, r *http.Reques
|
||||
for _, existing := range s.settings.GetStoragePaths() {
|
||||
if system.PathsOverlap(path, existing.Path) {
|
||||
data["StorageError"] = fmt.Sprintf("Az útvonal átfedi a már regisztrált %s útvonalat.", existing.Path)
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1545,7 +1530,7 @@ func (s *Server) settingsStorageAddHandler(w http.ResponseWriter, r *http.Reques
|
||||
if err := s.settings.AddStoragePath(sp); err != nil {
|
||||
s.logger.Printf("[ERROR] [web] Failed to add storage path: %v", err)
|
||||
data["StorageError"] = "Hiba a mentés során."
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1562,13 +1547,13 @@ func (s *Server) settingsStorageRemoveHandler(w http.ResponseWriter, r *http.Req
|
||||
s.logger.Printf("[DEBUG] [web] settingsStorageRemoveHandler: path=%s from %s", path, r.RemoteAddr)
|
||||
}
|
||||
|
||||
data := s.settingsData()
|
||||
data := s.storagePageData()
|
||||
|
||||
// Check: apps using this path
|
||||
apps := s.appsUsingPath(path)
|
||||
if len(apps) > 0 {
|
||||
data["StorageError"] = fmt.Sprintf("Nem törölhető: az alábbi alkalmazások használják: %s", strings.Join(apps, ", "))
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1576,7 +1561,7 @@ func (s *Server) settingsStorageRemoveHandler(w http.ResponseWriter, r *http.Req
|
||||
for _, sp := range s.settings.GetStoragePaths() {
|
||||
if sp.Path == path && sp.IsDefault {
|
||||
data["StorageError"] = "Az alapértelmezett adattároló nem törölhető."
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -1584,13 +1569,13 @@ func (s *Server) settingsStorageRemoveHandler(w http.ResponseWriter, r *http.Req
|
||||
// Check: last path
|
||||
if len(s.settings.GetStoragePaths()) <= 1 {
|
||||
data["StorageError"] = "Az utolsó adattároló nem törölhető."
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.settings.RemoveStoragePath(path); err != nil {
|
||||
data["StorageError"] = "Hiba a törlés során."
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1645,17 +1630,17 @@ func (s *Server) settingsStorageLabelHandler(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
if label == "" || len(label) > 50 {
|
||||
data := s.settingsData()
|
||||
data := s.storagePageData()
|
||||
data["StorageError"] = "A megnevezés nem lehet üres és legfeljebb 50 karakter."
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.settings.SetStorageLabel(path, label); err != nil {
|
||||
s.logger.Printf("[ERROR] [web] Failed to set storage label: %v", err)
|
||||
data := s.settingsData()
|
||||
data := s.storagePageData()
|
||||
data["StorageError"] = "Hiba a megnevezés mentésekor."
|
||||
s.executeTemplate(w, r, "settings", data)
|
||||
s.executeTemplate(w, r, "storage", data)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -50,15 +50,80 @@ func getPage(t *testing.T, s *Server, path string) *httptest.ResponseRecorder {
|
||||
return rec
|
||||
}
|
||||
|
||||
// TestSettingsSplitPagesRender (D1 Scenario A skeleton): the four pages respond 200.
|
||||
// Per-page unique-section markers are asserted once the template split lands (Part 2).
|
||||
// TestSettingsSplitPagesRender (D1 Scenario A): each page responds 200, carries its own
|
||||
// sections, and does NOT carry another page's sections (no cross-leak).
|
||||
func TestSettingsSplitPagesRender(t *testing.T) {
|
||||
s := testPageServer(t)
|
||||
for _, path := range []string{"/settings", "/settings/notifications", "/settings/security", "/storage"} {
|
||||
rec := getPage(t, s, path)
|
||||
if rec.Code != 200 {
|
||||
t.Errorf("GET %s = %d, want 200", path, rec.Code)
|
||||
cases := []struct {
|
||||
path string
|
||||
must []string
|
||||
mustNot []string
|
||||
}{
|
||||
{"/settings",
|
||||
[]string{"Rendszer konfiguráció", "Verzió és frissítés", "Vezérlő újraindítása", "Kiszolgáló újraindítása"},
|
||||
[]string{"Adattárolók", "Jelszó módosítás", "Értesítési szünet"}},
|
||||
{"/settings/notifications",
|
||||
[]string{"Beállítások — Értesítések"},
|
||||
[]string{"Rendszer konfiguráció", "Adattárolók", "Jelszó módosítás"}},
|
||||
{"/settings/security",
|
||||
[]string{"Jelszó módosítás", "Földrajzi korlátozás"},
|
||||
[]string{"Rendszer konfiguráció", "Adattárolók", "Értesítési szünet"}},
|
||||
{"/storage",
|
||||
[]string{"Adattárolók", "Hálózati tárhely (NAS)"},
|
||||
[]string{"Rendszer konfiguráció", "Jelszó módosítás", "Értesítési szünet"}},
|
||||
}
|
||||
for _, c := range cases {
|
||||
rec := getPage(t, s, c.path)
|
||||
if rec.Code != 200 {
|
||||
t.Errorf("GET %s = %d, want 200", 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">
|
||||
<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="/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="/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}}
|
||||
</ul>
|
||||
<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">
|
||||
<span class="version">{{.Version}}</span>
|
||||
{{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" .}}
|
||||
|
||||
<div class="page-header">
|
||||
<h2>Beállítások</h2>
|
||||
<h2>Tárhely</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: Storage Paths -->
|
||||
<div class="settings-card">
|
||||
<h3>Adattárolók</h3>
|
||||
@@ -372,6 +162,11 @@ pollUntilBack();
|
||||
</div>
|
||||
{{end}}
|
||||
</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
|
||||
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.
|
||||
(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>
|
||||
{{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">
|
||||
<a href="/settings/storage/init" class="btn btn-sm btn-outline">🔧 Ú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/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="/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 style="margin-top:1.5rem">
|
||||
@@ -733,597 +523,7 @@ window.__registeredPaths=[{{range .StoragePaths}}{{if .Path}}"{{.Path}}",{{end}}
|
||||
</details>
|
||||
</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 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) {
|
||||
var wrap = document.getElementById('label-wrap-' + path);
|
||||
if (!wrap) return;
|
||||
@@ -1976,26 +1976,6 @@ a.stat-card:hover {
|
||||
margin-top: auto;
|
||||
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 --- */
|
||||
.storage-overview-grid {
|
||||
@@ -3244,3 +3224,21 @@ span.mono, .mono { font-family: var(--font-data); }
|
||||
.login-title .title-accent {
|
||||
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