feat(firewall): Inline-Note + Labels direkt in der Tabelle

- Migration 0035: note TEXT + labels TEXT[] für firewall_rules + nat_rules
- PATCH /firewall/rules/:id + /nat-rules/:id für note/labels Updates
- InlineNote: gold Tag mit MessageOutlined, Klick zum Bearbeiten (Enter/Blur speichert)
- InlineLabels: geekblue Tags mit X-Button zum Entfernen, "+" zum Hinzufügen
- Name-Spalte: Name + Labels + Note in Zeile 1, comment/auto-desc in Zeile 2
- Gleiche UX für NAT-Regeln

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Debian
2026-05-31 18:46:54 +02:00
parent 13cb9a8fc4
commit b48ba65ce3
11 changed files with 425 additions and 33 deletions

View File

@@ -138,6 +138,7 @@ func (h *FirewallHandler) Register(rg *gin.RouterGroup) {
rl.POST("", h.CreateRule)
rl.GET("/:id", h.GetRule)
rl.PUT("/:id", h.UpdateRule)
rl.PATCH("/:id", h.PatchRule)
rl.DELETE("/:id", h.DeleteRule)
nat := g.Group("/nat-rules")
@@ -145,6 +146,7 @@ func (h *FirewallHandler) Register(rg *gin.RouterGroup) {
nat.POST("", h.CreateNAT)
nat.GET("/:id", h.GetNAT)
nat.PUT("/:id", h.UpdateNAT)
nat.PATCH("/:id", h.PatchNAT)
nat.DELETE("/:id", h.DeleteNAT)
}
@@ -758,6 +760,48 @@ func (h *FirewallHandler) DeleteRule(c *gin.Context) {
response.NoContent(c); h.reload(c.Request.Context(), "delete")
}
func (h *FirewallHandler) PatchRule(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
var body struct {
Note *string `json:"note"`
Labels []string `json:"labels"`
}
if err := c.ShouldBindJSON(&body); err != nil {
response.BadRequest(c, err)
return
}
ctx := c.Request.Context()
if body.Note != nil {
if err := h.Rules.PatchNote(ctx, id, *body.Note); err != nil {
if errors.Is(err, firewall.ErrRuleNotFound) {
response.NotFound(c, err)
return
}
response.Internal(c, err)
return
}
}
if body.Labels != nil {
if err := h.Rules.PatchLabels(ctx, id, body.Labels); err != nil {
if errors.Is(err, firewall.ErrRuleNotFound) {
response.NotFound(c, err)
return
}
response.Internal(c, err)
return
}
}
out, err := h.Rules.Get(ctx, id)
if err != nil {
response.Internal(c, err)
return
}
response.OK(c, out)
}
// ── NAT Rules ──────────────────────────────────────────────────────────
func (h *FirewallHandler) ListNAT(c *gin.Context) {
@@ -858,6 +902,48 @@ func (h *FirewallHandler) DeleteNAT(c *gin.Context) {
response.NoContent(c); h.reload(c.Request.Context(), "delete")
}
func (h *FirewallHandler) PatchNAT(c *gin.Context) {
id, ok := parseID(c)
if !ok {
return
}
var body struct {
Note *string `json:"note"`
Labels []string `json:"labels"`
}
if err := c.ShouldBindJSON(&body); err != nil {
response.BadRequest(c, err)
return
}
ctx := c.Request.Context()
if body.Note != nil {
if err := h.NATRules.PatchNote(ctx, id, *body.Note); err != nil {
if errors.Is(err, firewall.ErrNATRuleNotFound) {
response.NotFound(c, err)
return
}
response.Internal(c, err)
return
}
}
if body.Labels != nil {
if err := h.NATRules.PatchLabels(ctx, id, body.Labels); err != nil {
if errors.Is(err, firewall.ErrNATRuleNotFound) {
response.NotFound(c, err)
return
}
response.Internal(c, err)
return
}
}
out, err := h.NATRules.Get(ctx, id)
if err != nil {
response.Internal(c, err)
return
}
response.OK(c, out)
}
// ── Validators ─────────────────────────────────────────────────────────
func validateAddrObjValue(kind, value string) error {