diff --git a/internal/api/admin_services_handlers.go b/internal/api/admin_services_handlers.go index 898d963..2d03b99 100644 --- a/internal/api/admin_services_handlers.go +++ b/internal/api/admin_services_handlers.go @@ -17,6 +17,7 @@ import ( var allowedServices = []string{ "archivmail", "archivmail-web", + "manticore", "postgresql@17-main", "postfix", "nginx", @@ -29,9 +30,57 @@ type ServiceStatus struct { Sub string `json:"sub"` // running, dead, exited, ... Enabled string `json:"enabled"` // enabled, disabled, static, unknown Description string `json:"description"` + Version string `json:"version,omitempty"` // best-effort, empty if not determinable ExternalBlocked *bool `json:"external_blocked,omitempty"` // only set for archivmail } +// serviceVersion resolves the installed version of a service, best-effort. +// Superadmin-visible "which version is actually running" overview — before +// this, the only way to see e.g. the Manticore version after an upgrade +// (PROJ-67) was SSH + `searchd --version`. Errors are swallowed on purpose: +// an unknown version must never turn the whole services list red. +func (s *Server) serviceVersion(name string) string { + switch name { + case "archivmail", "archivmail-web": + return s.appVersion + case "manticore": + out, err := exec.Command("searchd", "--version").CombinedOutput() + if err != nil { + return "" + } + return firstLine(string(out)) + case "postgresql@17-main": + out, err := exec.Command("psql", "--version").Output() + if err != nil { + return "" + } + return firstLine(string(out)) + case "postfix": + out, err := exec.Command("postconf", "mail_version").Output() + if err != nil { + return "" + } + _, v, ok := strings.Cut(firstLine(string(out)), "=") + if !ok { + return "" + } + return strings.TrimSpace(v) + case "nginx": + out, err := exec.Command("nginx", "-v").CombinedOutput() + if err != nil { + return "" + } + return firstLine(string(out)) + default: + return "" + } +} + +func firstLine(s string) string { + line, _, _ := strings.Cut(s, "\n") + return strings.TrimSpace(line) +} + func isAllowedService(name string) bool { for _, s := range allowedServices { if s == name { @@ -41,7 +90,7 @@ func isAllowedService(name string) bool { return false } -func systemctlShow(name string) ServiceStatus { +func (s *Server) systemctlShow(name string) ServiceStatus { svc := ServiceStatus{Name: name, DisplayName: name} out, err := exec.Command("systemctl", "show", name+".service", "--property=ActiveState,SubState,UnitFileState,Description", @@ -72,6 +121,7 @@ func systemctlShow(name string) ServiceStatus { blocked := nftAPIBlocked() svc.ExternalBlocked = &blocked } + svc.Version = s.serviceVersion(name) return svc } @@ -87,7 +137,7 @@ func nftAPIBlocked() bool { func (s *Server) handleListServices(w http.ResponseWriter, r *http.Request) { result := make([]ServiceStatus, 0, len(allowedServices)) for _, name := range allowedServices { - result = append(result, systemctlShow(name)) + result = append(result, s.systemctlShow(name)) } writeJSON(w, http.StatusOK, result) } @@ -142,7 +192,7 @@ func (s *Server) handleServiceAction(w http.ResponseWriter, r *http.Request) { Detail: name, Success: true, }) - writeJSON(w, http.StatusOK, systemctlShow(name)) + writeJSON(w, http.StatusOK, s.systemctlShow(name)) return } @@ -166,5 +216,5 @@ func (s *Server) handleServiceAction(w http.ResponseWriter, r *http.Request) { Success: true, }) - writeJSON(w, http.StatusOK, systemctlShow(name)) + writeJSON(w, http.StatusOK, s.systemctlShow(name)) } diff --git a/src/components/admin/tabs/ServicesTab.tsx b/src/components/admin/tabs/ServicesTab.tsx index 8dec97c..f225e1e 100644 --- a/src/components/admin/tabs/ServicesTab.tsx +++ b/src/components/admin/tabs/ServicesTab.tsx @@ -66,6 +66,7 @@ export function ServicesTab({ Status Autostart Externer Zugriff + Version Beschreibung {isSuperAdmin && Aktionen} @@ -115,6 +116,9 @@ export function ServicesTab({ )} + + {svc.version || "–"} + {svc.description || "–"} diff --git a/src/lib/api/system.ts b/src/lib/api/system.ts index 4e1de81..c306242 100644 --- a/src/lib/api/system.ts +++ b/src/lib/api/system.ts @@ -43,6 +43,7 @@ export interface ServiceStatus { sub: string; enabled: string; description: string; + version?: string; external_blocked?: boolean; }