feat: Versions-Spalte im Dienste-Tab für Superadmin
Bislang war die einzige Möglichkeit, die laufende Manticore-/PostgreSQL-/ Postfix-/nginx-Version zu sehen, SSH + <binary> --version. serviceVersion() löst das best-effort pro Dienst auf (archivmail/-web: appVersion-Konstante, manticore: searchd --version, postgresql: psql --version, postfix: postconf mail_version, nginx: nginx -v). Fehler werden verschluckt (leerer String), eine unbekannte Version darf den Dienst-Status nicht auf "Fehler" kippen. manticore war bisher gar nicht in der Dienste-Whitelist (allowedServices) — jetzt ergänzt, damit es überhaupt in der Liste auftaucht und start/stop/restart wie die anderen Dienste möglich ist.
This commit is contained in:
@@ -17,6 +17,7 @@ import (
|
|||||||
var allowedServices = []string{
|
var allowedServices = []string{
|
||||||
"archivmail",
|
"archivmail",
|
||||||
"archivmail-web",
|
"archivmail-web",
|
||||||
|
"manticore",
|
||||||
"postgresql@17-main",
|
"postgresql@17-main",
|
||||||
"postfix",
|
"postfix",
|
||||||
"nginx",
|
"nginx",
|
||||||
@@ -29,9 +30,57 @@ type ServiceStatus struct {
|
|||||||
Sub string `json:"sub"` // running, dead, exited, ...
|
Sub string `json:"sub"` // running, dead, exited, ...
|
||||||
Enabled string `json:"enabled"` // enabled, disabled, static, unknown
|
Enabled string `json:"enabled"` // enabled, disabled, static, unknown
|
||||||
Description string `json:"description"`
|
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
|
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 {
|
func isAllowedService(name string) bool {
|
||||||
for _, s := range allowedServices {
|
for _, s := range allowedServices {
|
||||||
if s == name {
|
if s == name {
|
||||||
@@ -41,7 +90,7 @@ func isAllowedService(name string) bool {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func systemctlShow(name string) ServiceStatus {
|
func (s *Server) systemctlShow(name string) ServiceStatus {
|
||||||
svc := ServiceStatus{Name: name, DisplayName: name}
|
svc := ServiceStatus{Name: name, DisplayName: name}
|
||||||
out, err := exec.Command("systemctl", "show", name+".service",
|
out, err := exec.Command("systemctl", "show", name+".service",
|
||||||
"--property=ActiveState,SubState,UnitFileState,Description",
|
"--property=ActiveState,SubState,UnitFileState,Description",
|
||||||
@@ -72,6 +121,7 @@ func systemctlShow(name string) ServiceStatus {
|
|||||||
blocked := nftAPIBlocked()
|
blocked := nftAPIBlocked()
|
||||||
svc.ExternalBlocked = &blocked
|
svc.ExternalBlocked = &blocked
|
||||||
}
|
}
|
||||||
|
svc.Version = s.serviceVersion(name)
|
||||||
return svc
|
return svc
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +137,7 @@ func nftAPIBlocked() bool {
|
|||||||
func (s *Server) handleListServices(w http.ResponseWriter, r *http.Request) {
|
func (s *Server) handleListServices(w http.ResponseWriter, r *http.Request) {
|
||||||
result := make([]ServiceStatus, 0, len(allowedServices))
|
result := make([]ServiceStatus, 0, len(allowedServices))
|
||||||
for _, name := range allowedServices {
|
for _, name := range allowedServices {
|
||||||
result = append(result, systemctlShow(name))
|
result = append(result, s.systemctlShow(name))
|
||||||
}
|
}
|
||||||
writeJSON(w, http.StatusOK, result)
|
writeJSON(w, http.StatusOK, result)
|
||||||
}
|
}
|
||||||
@@ -142,7 +192,7 @@ func (s *Server) handleServiceAction(w http.ResponseWriter, r *http.Request) {
|
|||||||
Detail: name,
|
Detail: name,
|
||||||
Success: true,
|
Success: true,
|
||||||
})
|
})
|
||||||
writeJSON(w, http.StatusOK, systemctlShow(name))
|
writeJSON(w, http.StatusOK, s.systemctlShow(name))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,5 +216,5 @@ func (s *Server) handleServiceAction(w http.ResponseWriter, r *http.Request) {
|
|||||||
Success: true,
|
Success: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
writeJSON(w, http.StatusOK, systemctlShow(name))
|
writeJSON(w, http.StatusOK, s.systemctlShow(name))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ export function ServicesTab({
|
|||||||
<TableHead className="w-28">Status</TableHead>
|
<TableHead className="w-28">Status</TableHead>
|
||||||
<TableHead className="w-24">Autostart</TableHead>
|
<TableHead className="w-24">Autostart</TableHead>
|
||||||
<TableHead className="w-28">Externer Zugriff</TableHead>
|
<TableHead className="w-28">Externer Zugriff</TableHead>
|
||||||
|
<TableHead className="w-56">Version</TableHead>
|
||||||
<TableHead>Beschreibung</TableHead>
|
<TableHead>Beschreibung</TableHead>
|
||||||
{isSuperAdmin && <TableHead className="w-72 text-right">Aktionen</TableHead>}
|
{isSuperAdmin && <TableHead className="w-72 text-right">Aktionen</TableHead>}
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -115,6 +116,9 @@ export function ServicesTab({
|
|||||||
<span className="text-xs text-muted-foreground">–</span>
|
<span className="text-xs text-muted-foreground">–</span>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell className="text-xs font-mono text-muted-foreground truncate max-w-xs" title={svc.version || undefined}>
|
||||||
|
{svc.version || "–"}
|
||||||
|
</TableCell>
|
||||||
<TableCell className="text-sm text-muted-foreground truncate max-w-xs">
|
<TableCell className="text-sm text-muted-foreground truncate max-w-xs">
|
||||||
{svc.description || "–"}
|
{svc.description || "–"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ export interface ServiceStatus {
|
|||||||
sub: string;
|
sub: string;
|
||||||
enabled: string;
|
enabled: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
version?: string;
|
||||||
external_blocked?: boolean;
|
external_blocked?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user