package web import ( "fmt" "net/http" "net/url" "strings" "gitea.dooplex.hu/admin/felhom-controller/internal/scheduler" "gitea.dooplex.hu/admin/felhom-controller/internal/settings" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" "gitea.dooplex.hu/admin/felhom-controller/internal/system" "golang.org/x/crypto/bcrypt" ) func (s *Server) baseData(page, title string) map[string]interface{} { data := map[string]interface{}{ "Page": page, "Title": title, "CustomerName": s.cfg.Customer.Name, "Domain": s.cfg.Customer.Domain, "Version": s.version, "AuthEnabled": s.authEnabled(), } if s.alertManager != nil { data["Alerts"] = s.alertManager.GetAlerts() } return data } func (s *Server) dashboardHandler(w http.ResponseWriter, _ *http.Request) { stackList := s.stackMgr.GetStacks() // Filter to deployed + protected stacks first var deployedStacks []stacks.Stack for _, st := range stackList { if st.Deployed || st.Protected { deployedStacks = append(deployedStacks, st) } } // Count from the DISPLAYED set only running, stopped := 0, 0 for _, st := range deployedStacks { switch st.State { case stacks.StateRunning, stacks.StateStarting, stacks.StateUnhealthy, stacks.StateRestarting: running++ case stacks.StateStopped, stacks.StateExited: stopped++ } } sysInfo := system.GetInfo(s.cfg.Paths.HDDPath, s.cpuCollector) data := s.baseData("dashboard", "Vezérlőpult") data["Stacks"] = deployedStacks data["RunningCount"] = running data["StoppedCount"] = stopped data["TotalCount"] = len(stackList) data["SystemInfo"] = sysInfo // Backup status data["BackupEnabled"] = s.cfg.Backup.Enabled if s.backupMgr != nil { nextDBDump := scheduler.NextDailyRun(s.cfg.Backup.DBDumpSchedule) nextBackup := scheduler.NextDailyRun(s.cfg.Backup.ResticSchedule) fullStatus := s.backupMgr.GetFullStatus(nextDBDump, nextBackup) data["DBDumpStatus"] = fullStatus.LastDBDump data["BackupStatus"] = fullStatus.LastBackup data["BackupRunning"] = fullStatus.Running data["BackupMaxAgeHours"] = s.cfg.Monitoring.Thresholds.BackupMaxAgeHours } s.render(w, "dashboard", data) } func (s *Server) stacksHandler(w http.ResponseWriter, _ *http.Request) { data := s.baseData("stacks", "Alkalmazások") data["Stacks"] = s.stackMgr.GetStacks() s.render(w, "stacks", data) } func (s *Server) logsHandler(w http.ResponseWriter, r *http.Request, name string) { stack, ok := s.stackMgr.GetStack(name) if !ok { http.NotFound(w, r) return } logs, err := s.stackMgr.GetLogs(name, 200) if err != nil { logs = fmt.Sprintf("Hiba a naplók lekérésekor: %v", err) } // Raw mode: return plain text for AJAX polling if r.URL.Query().Get("raw") == "1" { w.Header().Set("Content-Type", "text/plain; charset=utf-8") fmt.Fprint(w, logs) return } data := s.baseData("logs", stack.Meta.DisplayName+" — Naplók") data["Stack"] = stack data["Logs"] = logs s.render(w, "logs", data) } func (s *Server) deployHandler(w http.ResponseWriter, r *http.Request, name string) { meta, appCfg, err := s.stackMgr.GetDeployFields(name) if err != nil { http.NotFound(w, r) return } stack, _ := s.stackMgr.GetStack(name) alreadyDeployed := appCfg != nil && appCfg.Deployed data := s.baseData("deploy", meta.DisplayName+" — Telepítés") data["Stack"] = stack data["Meta"] = meta data["AppConfig"] = appCfg data["AlreadyDeployed"] = alreadyDeployed data["LogoURL"] = s.cfg.AppLogoURL(meta.Slug) data["LogoPNGURL"] = s.cfg.AppLogoPNGURL(meta.Slug) data["AppPageURL"] = s.cfg.AppPageURL(meta.Slug) data["UserFields"] = meta.UserFacingFields() data["AutoFields"] = meta.AutoGeneratedFields() // Memory info for deploy page (only for non-deployed apps) if !alreadyDeployed { memInfo := map[string]interface{}{"Available": false} totalMB, memErr := system.GetTotalMemoryMB() if memErr == nil { reservedMB := s.cfg.System.ReservedMemoryMB usableMB := totalMB - reservedMB committedReqMB, committedLimitMB := s.stackMgr.CommittedMemory() newReqMB := stacks.ParseMemoryMB(meta.Resources.MemRequest) newLimitMB := stacks.ParseMemoryMB(meta.Resources.MemLimit) afterReqMB := committedReqMB + newReqMB afterLimitMB := committedLimitMB + newLimitMB percent := 0 if usableMB > 0 { percent = afterReqMB * 100 / usableMB } committedPercent := 0 if usableMB > 0 { committedPercent = committedReqMB * 100 / usableMB } memInfo["Available"] = true memInfo["TotalMB"] = totalMB memInfo["ReservedMB"] = reservedMB memInfo["UsableMB"] = usableMB memInfo["CommittedMB"] = committedReqMB memInfo["NewRequestMB"] = newReqMB memInfo["AfterMB"] = afterReqMB memInfo["Percent"] = percent memInfo["CommittedPercent"] = committedPercent memInfo["Blocked"] = newReqMB > 0 && afterReqMB > usableMB memInfo["OvercommitWarn"] = newLimitMB > 0 && afterLimitMB > totalMB } data["MemoryInfo"] = memInfo } s.render(w, "deploy", data) } func (s *Server) appDetailHandler(w http.ResponseWriter, r *http.Request, slug string) { var found *stacks.Stack for _, stack := range s.stackMgr.GetStacks() { if stack.Meta.Slug == slug { found = &stack break } } if found == nil { http.NotFound(w, r) return } // Load current optional config values from app.yaml currentValues := make(map[string]string) if appCfg := s.stackMgr.LoadAppConfigByName(found.Name); appCfg != nil { for k, v := range appCfg.Env { currentValues[k] = v } } data := s.baseData("stacks", found.Meta.DisplayName) data["Stack"] = found data["Meta"] = found.Meta data["AppInfo"] = found.Meta.AppInfo data["OptionalConfig"] = found.Meta.OptionalConfig data["CurrentValues"] = currentValues data["HasAppInfo"] = found.Meta.HasAppInfo() data["HasOptionalConfig"] = found.Meta.HasOptionalConfig() s.render(w, "app_info", data) } func (s *Server) monitoringHandler(w http.ResponseWriter, _ *http.Request) { data := s.baseData("monitoring", "Rendszermonitor") data["SystemInfo"] = system.GetInfo(s.cfg.Paths.HDDPath, s.cpuCollector) // On monitoring page, exclude the "pings-missing" alert since the detailed table is visible if s.alertManager != nil { data["Alerts"] = s.alertManager.GetAlerts("pings-missing") } // Ping status section data["MonitoringEnabled"] = s.cfg.Monitoring.Enabled if s.cfg.Monitoring.Enabled { pings := []map[string]interface{}{ {"Label": "Életjel (Heartbeat)", "Icon": "💓", "Configured": isPingConfigured(s.cfg.Monitoring.PingUUIDs.Heartbeat), "Schedule": "5 percenként"}, {"Label": "Rendszer állapot", "Icon": "🖥️", "Configured": isPingConfigured(s.cfg.Monitoring.PingUUIDs.SystemHealth), "Schedule": "5 percenként"}, {"Label": "Adatbázis mentés", "Icon": "🗄️", "Configured": isPingConfigured(s.cfg.Monitoring.PingUUIDs.DBDump), "Schedule": "Naponta " + s.cfg.Backup.DBDumpSchedule}, {"Label": "Biztonsági mentés", "Icon": "💾", "Configured": isPingConfigured(s.cfg.Monitoring.PingUUIDs.Backup), "Schedule": "Naponta " + s.cfg.Backup.ResticSchedule}, {"Label": "Mentés integritás", "Icon": "🔍", "Configured": isPingConfigured(s.cfg.Monitoring.PingUUIDs.BackupIntegrity), "Schedule": "Hetente (vasárnap)"}, } allConfigured := true for _, p := range pings { if !p["Configured"].(bool) { allConfigured = false break } } data["PingStatus"] = pings data["AllPingsConfigured"] = allConfigured } s.render(w, "monitoring", data) } // isPingConfigured returns true if a healthcheck ping UUID is non-empty and not a placeholder. func isPingConfigured(uuid string) bool { return uuid != "" && !strings.HasPrefix(uuid, "CHANGEME") } func (s *Server) backupsHandler(w http.ResponseWriter, _ *http.Request) { data := s.baseData("backups", "Biztonsági mentés") if s.backupMgr != nil { nextDBDump := scheduler.NextDailyRun(s.cfg.Backup.DBDumpSchedule) nextBackup := scheduler.NextDailyRun(s.cfg.Backup.ResticSchedule) fullStatus := s.backupMgr.GetFullStatus(nextDBDump, nextBackup) data["Backup"] = fullStatus } else { data["Backup"] = nil } s.render(w, "backups", data) } func (s *Server) settingsData() map[string]interface{} { data := s.baseData("settings", "Beállítások") data["CustomerID"] = s.cfg.Customer.ID data["CustomerDomain"] = s.cfg.Customer.Domain data["GitRepoURL"] = s.cfg.Git.RepoURL data["GitSyncInterval"] = s.cfg.Git.SyncInterval data["BackupEnabled"] = s.cfg.Backup.Enabled data["DBDumpSchedule"] = s.cfg.Backup.DBDumpSchedule data["ResticSchedule"] = s.cfg.Backup.ResticSchedule data["MonitoringEnabled"] = s.cfg.Monitoring.Enabled data["HealthchecksBase"] = s.cfg.Monitoring.HealthchecksBase data["HubEnabled"] = s.cfg.Hub.Enabled data["NotificationPrefs"] = s.settings.GetNotificationPrefs() return data } func (s *Server) settingsHandler(w http.ResponseWriter, _ *http.Request) { s.render(w, "settings", s.settingsData()) } func (s *Server) settingsPasswordHandler(w http.ResponseWriter, r *http.Request) { _ = r.ParseForm() currentPassword := r.FormValue("current_password") newPassword := r.FormValue("new_password") confirmPassword := r.FormValue("confirm_password") data := s.settingsData() // Validate current password effectiveHash := s.effectivePasswordHash() if err := bcrypt.CompareHashAndPassword([]byte(effectiveHash), []byte(currentPassword)); err != nil { data["PasswordError"] = "Hibás jelenlegi jelszó" s.render(w, "settings", data) return } // Validate new password length if len(newPassword) < 8 { data["PasswordError"] = "A jelszónak legalább 8 karakter hosszúnak kell lennie" s.render(w, "settings", data) return } // Validate passwords match if newPassword != confirmPassword { data["PasswordError"] = "A két jelszó nem egyezik" s.render(w, "settings", data) return } // Generate bcrypt hash hash, err := bcrypt.GenerateFromPassword([]byte(newPassword), 10) if err != nil { s.logger.Printf("[ERROR] Failed to hash new password: %v", err) data["PasswordError"] = "Belső hiba a jelszó mentésekor" s.render(w, "settings", data) return } // Save to settings.json if err := s.settings.SetPasswordHash(string(hash)); err != nil { s.logger.Printf("[ERROR] Failed to save password to settings.json: %v", err) data["PasswordError"] = "Belső hiba a jelszó mentésekor" s.render(w, "settings", data) return } s.logger.Printf("[INFO] Password changed via settings page from %s", r.RemoteAddr) // Invalidate all sessions (force re-login) s.invalidateAllSessions() // Redirect to login with flash message flash := url.QueryEscape("Jelszó sikeresen módosítva. Kérjük, jelentkezzen be az új jelszóval.") http.Redirect(w, r, "/login?flash="+flash, http.StatusFound) } func (s *Server) settingsNotificationsHandler(w http.ResponseWriter, r *http.Request) { _ = r.ParseForm() email := strings.TrimSpace(r.FormValue("notification_email")) cooldownStr := r.FormValue("cooldown_hours") cooldownHours := 6 if cooldownStr != "" { if n, err := fmt.Sscanf(cooldownStr, "%d", &cooldownHours); n != 1 || err != nil { cooldownHours = 6 } } if cooldownHours < 1 { cooldownHours = 1 } if cooldownHours > 168 { cooldownHours = 168 } // Collect enabled events from checkboxes var enabledEvents []string for _, evt := range []string{"disk_warning", "backup_failed", "update_available", "security_update"} { if r.FormValue("event_"+evt) == "on" { enabledEvents = append(enabledEvents, evt) } } prefs := &settings.NotificationPrefs{ Email: email, EnabledEvents: enabledEvents, CooldownHours: cooldownHours, } if err := s.settings.SetNotificationPrefs(prefs); err != nil { s.logger.Printf("[ERROR] Failed to save notification prefs: %v", err) data := s.settingsData() data["NotificationError"] = "Hiba a beállítások mentésekor" s.render(w, "settings", data) return } s.logger.Printf("[INFO] Notification preferences updated: email=%s, events=%v", email, enabledEvents) // Sync preferences to hub data := s.settingsData() if s.notifier != nil && s.notifier.IsEnabled() { if err := s.notifier.SyncPreferences(email, enabledEvents); err != nil { s.logger.Printf("[WARN] Failed to sync preferences to hub: %v", err) data["NotificationSuccess"] = fmt.Sprintf("Értesítési beállítások mentve (helyi). A központi szinkronizálás sikertelen: %v", err) } else { data["NotificationSuccess"] = "Értesítési beállítások mentve." } } else { data["NotificationSuccess"] = "Értesítési beállítások mentve." } s.render(w, "settings", data) } func (s *Server) settingsNotificationsTestHandler(w http.ResponseWriter, r *http.Request) { data := s.settingsData() if s.notifier == nil { data["NotificationError"] = "Az értesítések nincsenek bekapcsolva" s.render(w, "settings", data) return } err := s.notifier.SendTest() if err != nil { s.logger.Printf("[ERROR] Test notification failed: %v", err) data["NotificationError"] = fmt.Sprintf("Teszt email küldése sikertelen: %v", err) s.render(w, "settings", data) return } data["NotificationSuccess"] = "Teszt email elküldve." s.render(w, "settings", data) }