feat(ui): Quick-Toggles, Config-Preview, Dashboard-Alerts, Domain-Detail-Health
Quick-Toggle-Switches (kein Modal nötig) für: Backends, Backend-Server, DNS-Zonen, DNS-Records, Domains (active), Firewall-Rules, NAT-Rules, Forward-Proxy ACLs, Routing-Rules. Dashboard: Alert-Banner für komplett ausgefallene Backends (HAProxy-Stats) und Domains im Maintenance-Mode. Domain-Detail: HAProxy-Live-Health-Badge (15s Polling), TLS-Cert ausstellen/erneuern direkt aus dem Detail, Routing-Rules-Panel inline. Config-Preview (Settings): alle 4 Generatoren (haproxy, nftables, squid, unbound) rendern via RenderToString ohne Disk-Write — GET /system/config-preview. ActionButtons: Viewer-Rolle blendet Delete aus (RBAC-Ergänzung). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -52,6 +52,18 @@ func New(pool *pgxpool.Pool) *Generator { return &Generator{Pool: pool} }
|
||||
|
||||
func (g *Generator) Name() string { return "nftables" }
|
||||
|
||||
func (g *Generator) RenderToString(ctx context.Context) (string, error) {
|
||||
view, err := g.loadView(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("nftables: load state: %w", err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := tpl.Execute(&buf, view); err != nil {
|
||||
return "", fmt.Errorf("nftables: render template: %w", err)
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (g *Generator) Render(ctx context.Context) error {
|
||||
view, err := g.loadView(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -36,6 +36,10 @@ type SystemHandler struct {
|
||||
Audit *audit.Repo
|
||||
NodeID string
|
||||
Pool *pgxpool.Pool
|
||||
// ConfigPreviewers: generator name → render-to-string func. Wired
|
||||
// in main.go after generators are constructed. Read-only access —
|
||||
// no writes, no reloads.
|
||||
ConfigPreviewers map[string]func(stdcontext.Context) (string, error)
|
||||
}
|
||||
|
||||
func NewSystemHandler(version string) *SystemHandler {
|
||||
@@ -64,6 +68,13 @@ func (h *SystemHandler) WithDB(pool *pgxpool.Pool) *SystemHandler {
|
||||
return h
|
||||
}
|
||||
|
||||
// WithConfigPreviewers injectet die RenderToString-Funktionen der
|
||||
// Config-Generatoren für den /system/config-preview Endpoint.
|
||||
func (h *SystemHandler) WithConfigPreviewers(previewers map[string]func(stdcontext.Context) (string, error)) *SystemHandler {
|
||||
h.ConfigPreviewers = previewers
|
||||
return h
|
||||
}
|
||||
|
||||
func (h *SystemHandler) Register(rg *gin.RouterGroup) {
|
||||
g := rg.Group("/system")
|
||||
g.GET("/health", h.Health)
|
||||
@@ -87,6 +98,7 @@ func (h *SystemHandler) Register(rg *gin.RouterGroup) {
|
||||
g.GET("/upgrade-status", h.UpgradeStatus)
|
||||
g.GET("/ipv6", h.IPv6)
|
||||
g.POST("/ipv6", h.SetIPv6)
|
||||
g.GET("/config-preview", h.ConfigPreview)
|
||||
}
|
||||
|
||||
// RegisterAgent mountet die read-only System-Endpoints auf der mTLS-
|
||||
@@ -680,6 +692,38 @@ func (h *SystemHandler) ToggleAutoUpdate(c *gin.Context) {
|
||||
response.OK(c, gin.H{"enabled": req.Enabled})
|
||||
}
|
||||
|
||||
// ConfigPreview rendert eine Service-Config aus dem aktuellen DB-State
|
||||
// und gibt sie als Plain-Text zurück — ohne sie auf Disk zu schreiben
|
||||
// oder den Dienst zu reloaden. Query-Parameter: ?generator=haproxy|nftables|squid|unbound
|
||||
func (h *SystemHandler) ConfigPreview(c *gin.Context) {
|
||||
gen := strings.TrimSpace(c.Query("generator"))
|
||||
if gen == "" {
|
||||
names := make([]string, 0, len(h.ConfigPreviewers))
|
||||
for k := range h.ConfigPreviewers {
|
||||
names = append(names, k)
|
||||
}
|
||||
response.OK(c, gin.H{"generators": names})
|
||||
return
|
||||
}
|
||||
if h.ConfigPreviewers == nil {
|
||||
response.Err(c, http.StatusServiceUnavailable, simpleErr("previewers not wired"))
|
||||
return
|
||||
}
|
||||
fn, ok := h.ConfigPreviewers[gen]
|
||||
if !ok {
|
||||
response.Err(c, http.StatusBadRequest, simpleErr("unknown generator: "+gen))
|
||||
return
|
||||
}
|
||||
ctx, cancel := stdcontext.WithTimeout(c.Request.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
content, err := fn(ctx)
|
||||
if err != nil {
|
||||
response.Internal(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, gin.H{"generator": gen, "content": content})
|
||||
}
|
||||
|
||||
func (h *SystemHandler) Health(c *gin.Context) {
|
||||
response.OK(c, gin.H{
|
||||
"status": "ok",
|
||||
|
||||
@@ -88,6 +88,18 @@ func New(pool *pgxpool.Pool) *Generator {
|
||||
|
||||
func (g *Generator) Name() string { return "haproxy" }
|
||||
|
||||
func (g *Generator) RenderToString(ctx context.Context) (string, error) {
|
||||
view, err := g.loadView(ctx)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("haproxy: load state: %w", err)
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := tpl.Execute(&buf, view); err != nil {
|
||||
return "", fmt.Errorf("haproxy: render template: %w", err)
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (g *Generator) Render(ctx context.Context) error {
|
||||
view, err := g.loadView(ctx)
|
||||
if err != nil {
|
||||
|
||||
@@ -47,15 +47,31 @@ func New(pool *pgxpool.Pool) *Generator {
|
||||
|
||||
func (g *Generator) Name() string { return "squid" }
|
||||
|
||||
func (g *Generator) Render(ctx context.Context) error {
|
||||
func (g *Generator) renderBuf(ctx context.Context) (bytes.Buffer, error) {
|
||||
acls, err := g.Repo.List(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("list acls: %w", err)
|
||||
return bytes.Buffer{}, fmt.Errorf("list acls: %w", err)
|
||||
}
|
||||
view := View{ListenPort: listenPort, ACLs: acls}
|
||||
var body bytes.Buffer
|
||||
if err := tpl.Execute(&body, view); err != nil {
|
||||
return fmt.Errorf("template: %w", err)
|
||||
return bytes.Buffer{}, fmt.Errorf("template: %w", err)
|
||||
}
|
||||
return body, nil
|
||||
}
|
||||
|
||||
func (g *Generator) RenderToString(ctx context.Context) (string, error) {
|
||||
buf, err := g.renderBuf(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
||||
func (g *Generator) Render(ctx context.Context) error {
|
||||
body, err := g.renderBuf(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(confPath), 0o755); err != nil {
|
||||
return fmt.Errorf("mkdir: %w", err)
|
||||
|
||||
@@ -93,16 +93,16 @@ func New(pool *pgxpool.Pool) *Generator {
|
||||
|
||||
func (g *Generator) Name() string { return "unbound" }
|
||||
|
||||
func (g *Generator) Render(ctx context.Context) error {
|
||||
func (g *Generator) buildView(ctx context.Context) (*View, error) {
|
||||
settings, err := g.Repo.GetSettings(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("settings: %w", err)
|
||||
return nil, fmt.Errorf("settings: %w", err)
|
||||
}
|
||||
zones, err := g.Repo.ListZones(ctx)
|
||||
if err != nil {
|
||||
return fmt.Errorf("zones: %w", err)
|
||||
return nil, fmt.Errorf("zones: %w", err)
|
||||
}
|
||||
view := View{
|
||||
view := &View{
|
||||
Settings: settings,
|
||||
ListenAddresses: splitCSV(settings.ListenAddresses),
|
||||
AccessACLs: splitCSV(settings.AccessACL),
|
||||
@@ -117,7 +117,7 @@ func (g *Generator) Render(ctx context.Context) error {
|
||||
case "local":
|
||||
recs, err := g.Repo.ListRecordsForZone(ctx, z.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("records for zone %s: %w", z.Name, err)
|
||||
return nil, fmt.Errorf("records for zone %s: %w", z.Name, err)
|
||||
}
|
||||
active := make([]recordView, 0, len(recs))
|
||||
for _, r := range recs {
|
||||
@@ -141,7 +141,26 @@ func (g *Generator) Render(ctx context.Context) error {
|
||||
})
|
||||
}
|
||||
}
|
||||
return view, nil
|
||||
}
|
||||
|
||||
func (g *Generator) RenderToString(ctx context.Context) (string, error) {
|
||||
view, err := g.buildView(ctx)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var body bytes.Buffer
|
||||
if err := tpl.Execute(&body, view); err != nil {
|
||||
return "", fmt.Errorf("template: %w", err)
|
||||
}
|
||||
return body.String(), nil
|
||||
}
|
||||
|
||||
func (g *Generator) Render(ctx context.Context) error {
|
||||
view, err := g.buildView(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var body bytes.Buffer
|
||||
if err := tpl.Execute(&body, view); err != nil {
|
||||
return fmt.Errorf("template: %w", err)
|
||||
|
||||
Reference in New Issue
Block a user