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;
}