Browse Source

feat(routing): implement MikroTik RouterOS v7 filter model with procedural syntax, select rules, community lists, and AS lists

Proxmox Router 1 tuần trước cách đây
mục cha
commit
8000ef9833

+ 20 - 0
internal/api/api.go

@@ -291,6 +291,26 @@ func (s *Server) registerRoutes() {
 	s.mux.HandleFunc("DELETE /api/routing/filters/{id}", s.handleDeleteRoutingFilter)
 	s.mux.HandleFunc("POST /api/routing/filters/{id}/state", s.handleSetRoutingFilterState)
 
+	s.mux.HandleFunc("GET /api/routing/filters/rules", s.handleListRoutingFilters)
+	s.mux.HandleFunc("POST /api/routing/filters/rules", s.handleSaveRoutingFilter)
+	s.mux.HandleFunc("DELETE /api/routing/filters/rules/{id}", s.handleDeleteRoutingFilter)
+	s.mux.HandleFunc("POST /api/routing/filters/rules/{id}/state", s.handleSetRoutingFilterState)
+
+	s.mux.HandleFunc("GET /api/routing/filters/select-rules", s.handleListSelectRules)
+	s.mux.HandleFunc("POST /api/routing/filters/select-rules", s.handleSaveSelectRule)
+	s.mux.HandleFunc("DELETE /api/routing/filters/select-rules/{id}", s.handleDeleteSelectRule)
+	s.mux.HandleFunc("POST /api/routing/filters/select-rules/{id}/state", s.handleSetSelectRuleState)
+
+	s.mux.HandleFunc("GET /api/routing/filters/community-lists", s.handleListCommunityLists)
+	s.mux.HandleFunc("POST /api/routing/filters/community-lists", s.handleSaveCommunityList)
+	s.mux.HandleFunc("DELETE /api/routing/filters/community-lists/{id}", s.handleDeleteCommunityList)
+	s.mux.HandleFunc("POST /api/routing/filters/community-lists/{id}/state", s.handleSetCommunityListState)
+
+	s.mux.HandleFunc("GET /api/routing/filters/as-lists", s.handleListASLists)
+	s.mux.HandleFunc("POST /api/routing/filters/as-lists", s.handleSaveASList)
+	s.mux.HandleFunc("DELETE /api/routing/filters/as-lists/{id}", s.handleDeleteASList)
+	s.mux.HandleFunc("POST /api/routing/filters/as-lists/{id}/state", s.handleSetASListState)
+
 	// Troubleshooting Tools (/tool ping, traceroute, mtr, dns, port-probe)
 	s.mux.HandleFunc("POST /api/tools/ping", s.handleToolsPing)
 	s.mux.HandleFunc("POST /api/tools/ping/stream", s.handleToolsPingStream)

+ 188 - 1
internal/api/dynamic_routing_handlers.go

@@ -3,6 +3,7 @@ package api
 import (
 	"encoding/json"
 	"net/http"
+	"strings"
 
 	"proxmox-router/internal/dynamicrouting"
 )
@@ -446,7 +447,7 @@ func (s *Server) handleSaveRoutingFilter(w http.ResponseWriter, r *http.Request)
 		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Chain name is required"})
 		return
 	}
-	if rule.Action == "" {
+	if rule.Action == "" && rule.Rule == "" {
 		rule.Action = "accept"
 	}
 	if err := s.dynRoutingMgr.SaveRoutingFilter(&rule); err != nil {
@@ -488,3 +489,189 @@ func (s *Server) handleSetRoutingFilterState(w http.ResponseWriter, r *http.Requ
 	}
 	writeJSON(w, http.StatusOK, map[string]interface{}{"status": "success", "disabled": req.Disabled})
 }
+
+// -------------------------------------------------------------
+// Select Rules Handlers (/routing filter select-rule)
+// -------------------------------------------------------------
+
+func (s *Server) handleListSelectRules(w http.ResponseWriter, r *http.Request) {
+	rules := s.dynRoutingMgr.ListSelectRules()
+	if rules == nil {
+		rules = []dynamicrouting.RoutingFilterSelectRule{}
+	}
+	writeJSON(w, http.StatusOK, rules)
+}
+
+func (s *Server) handleSaveSelectRule(w http.ResponseWriter, r *http.Request) {
+	var rule dynamicrouting.RoutingFilterSelectRule
+	if err := json.NewDecoder(r.Body).Decode(&rule); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON: " + err.Error()})
+		return
+	}
+	if rule.Chain == "" {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Chain name is required"})
+		return
+	}
+	if err := s.dynRoutingMgr.SaveSelectRule(&rule); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]string{"status": "success", "id": rule.ID})
+}
+
+func (s *Server) handleDeleteSelectRule(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Missing select rule ID"})
+		return
+	}
+	if err := s.dynRoutingMgr.DeleteSelectRule(id); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
+}
+
+func (s *Server) handleSetSelectRuleState(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Missing select rule ID"})
+		return
+	}
+	var req struct {
+		Disabled bool `json:"disabled"`
+	}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON"})
+		return
+	}
+	if err := s.dynRoutingMgr.SetSelectRuleState(id, req.Disabled); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]interface{}{"status": "success", "disabled": req.Disabled})
+}
+
+// -------------------------------------------------------------
+// Community Lists Handlers (/routing filter community-list)
+// -------------------------------------------------------------
+
+func (s *Server) handleListCommunityLists(w http.ResponseWriter, r *http.Request) {
+	lists := s.dynRoutingMgr.ListCommunityLists()
+	if lists == nil {
+		lists = []dynamicrouting.RoutingFilterCommunityList{}
+	}
+	writeJSON(w, http.StatusOK, lists)
+}
+
+func (s *Server) handleSaveCommunityList(w http.ResponseWriter, r *http.Request) {
+	var cl dynamicrouting.RoutingFilterCommunityList
+	if err := json.NewDecoder(r.Body).Decode(&cl); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON: " + err.Error()})
+		return
+	}
+	if strings.TrimSpace(cl.Name) == "" {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "List name is required"})
+		return
+	}
+	if err := s.dynRoutingMgr.SaveCommunityList(&cl); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]string{"status": "success", "id": cl.ID})
+}
+
+func (s *Server) handleDeleteCommunityList(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Missing community list ID"})
+		return
+	}
+	if err := s.dynRoutingMgr.DeleteCommunityList(id); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
+}
+
+func (s *Server) handleSetCommunityListState(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Missing community list ID"})
+		return
+	}
+	var req struct {
+		Disabled bool `json:"disabled"`
+	}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON"})
+		return
+	}
+	if err := s.dynRoutingMgr.SetCommunityListState(id, req.Disabled); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]interface{}{"status": "success", "disabled": req.Disabled})
+}
+
+// -------------------------------------------------------------
+// AS Lists Handlers (/routing filter num-list)
+// -------------------------------------------------------------
+
+func (s *Server) handleListASLists(w http.ResponseWriter, r *http.Request) {
+	lists := s.dynRoutingMgr.ListASLists()
+	if lists == nil {
+		lists = []dynamicrouting.RoutingFilterASList{}
+	}
+	writeJSON(w, http.StatusOK, lists)
+}
+
+func (s *Server) handleSaveASList(w http.ResponseWriter, r *http.Request) {
+	var al dynamicrouting.RoutingFilterASList
+	if err := json.NewDecoder(r.Body).Decode(&al); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON: " + err.Error()})
+		return
+	}
+	if strings.TrimSpace(al.Name) == "" {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "List name is required"})
+		return
+	}
+	if err := s.dynRoutingMgr.SaveASList(&al); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]string{"status": "success", "id": al.ID})
+}
+
+func (s *Server) handleDeleteASList(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Missing AS list ID"})
+		return
+	}
+	if err := s.dynRoutingMgr.DeleteASList(id); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
+}
+
+func (s *Server) handleSetASListState(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Missing AS list ID"})
+		return
+	}
+	var req struct {
+		Disabled bool `json:"disabled"`
+	}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		writeJSON(w, http.StatusBadRequest, map[string]string{"error": "Invalid JSON"})
+		return
+	}
+	if err := s.dynRoutingMgr.SetASListState(id, req.Disabled); err != nil {
+		writeJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
+		return
+	}
+	writeJSON(w, http.StatusOK, map[string]interface{}{"status": "success", "disabled": req.Disabled})
+}

+ 447 - 63
internal/dynamicrouting/manager.go

@@ -94,10 +94,13 @@ func (m *Manager) initDefaults() {
 				AreaType: "default",
 			},
 		},
-		OSPFAreaRanges: []OSPFAreaRange{},
-		OSPFInterfaces: []OSPFInterface{},
-		OSPFStaticNbrs: []OSPFStaticNeighbor{},
-		RoutingFilters: []RoutingFilterRule{},
+		OSPFAreaRanges:        []OSPFAreaRange{},
+		OSPFInterfaces:        []OSPFInterface{},
+		OSPFStaticNbrs:        []OSPFStaticNeighbor{},
+		RoutingFilters:        []RoutingFilterRule{},
+		RoutingSelectRules:    []RoutingFilterSelectRule{},
+		RoutingCommunityLists: []RoutingFilterCommunityList{},
+		RoutingASLists:        []RoutingFilterASList{},
 	}
 }
 
@@ -113,6 +116,16 @@ func (m *Manager) load() error {
 		return err
 	}
 
+	if m.config.RoutingSelectRules == nil {
+		m.config.RoutingSelectRules = []RoutingFilterSelectRule{}
+	}
+	if m.config.RoutingCommunityLists == nil {
+		m.config.RoutingCommunityLists = []RoutingFilterCommunityList{}
+	}
+	if m.config.RoutingASLists == nil {
+		m.config.RoutingASLists = []RoutingFilterASList{}
+	}
+
 	if len(m.config.OSPFInstances) == 0 {
 		rID := m.config.OSPFInstance.RouterID
 		if rID == "" {
@@ -165,7 +178,10 @@ func sanitizeIdentifier(prefix, raw string) string {
 	if cleaned == "" {
 		cleaned = "inst"
 	}
-	res := prefix + "_" + cleaned
+	res := cleaned
+	if prefix != "" {
+		res = prefix + "_" + cleaned
+	}
 	if len(res) > 32 {
 		res = res[:32]
 	}
@@ -222,6 +238,35 @@ func (m *Manager) applyBirdConfig() error {
 		sb.WriteString("}\n\n")
 	}
 
+	// Community Lists (/routing filter community-list)
+	for _, cl := range m.config.RoutingCommunityLists {
+		if cl.Disabled || strings.TrimSpace(cl.Name) == "" || len(cl.Communities) == 0 {
+			continue
+		}
+		var items []string
+		for _, c := range cl.Communities {
+			c = strings.TrimSpace(c)
+			if strings.Contains(c, ":") {
+				parts := strings.Split(c, ":")
+				items = append(items, fmt.Sprintf("(%s, %s)", strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])))
+			}
+		}
+		if len(items) > 0 {
+			sb.WriteString(fmt.Sprintf("define comm_%s = [ %s ];\n", sanitizeIdentifier("", cl.Name), strings.Join(items, ", ")))
+		}
+	}
+
+	// AS Lists (/routing filter num-list)
+	for _, al := range m.config.RoutingASLists {
+		if al.Disabled || strings.TrimSpace(al.Name) == "" || strings.TrimSpace(al.Range) == "" {
+			continue
+		}
+		rng := strings.TrimSpace(al.Range)
+		rng = regexp.MustCompile(`(\d+)\s*-\s*(\d+)`).ReplaceAllString(rng, "$1..$2")
+		sb.WriteString(fmt.Sprintf("define aslist_%s = [ %s ];\n", sanitizeIdentifier("", al.Name), rng))
+	}
+	sb.WriteString("\n")
+
 	// Routing Filters (/routing filter)
 	chainRules := make(map[string][]RoutingFilterRule)
 	for _, rule := range m.config.RoutingFilters {
@@ -241,69 +286,20 @@ func (m *Manager) applyBirdConfig() error {
 		sb.WriteString(fmt.Sprintf("filter %s {\n", filterName))
 
 		for _, r := range rules {
-			var conditions []string
-
-			if r.Prefix != "" {
-				pfx := strings.TrimSpace(r.Prefix)
-				pfxLen := strings.TrimSpace(r.PrefixLen)
-				if pfxLen != "" {
-					if strings.Contains(pfxLen, "-") {
-						parts := strings.Split(pfxLen, "-")
-						if len(parts) == 2 {
-							conditions = append(conditions, fmt.Sprintf("net ~ [ %s{%s,%s} ]", pfx, strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])))
-						}
-					} else {
-						conditions = append(conditions, fmt.Sprintf("net ~ [ %s{%s,32} ]", pfx, pfxLen))
+			rendered := translateRos7RuleToBird(&r)
+			if rendered != "" {
+				sb.WriteString(fmt.Sprintf("    # rule %d\n", r.RuleOrder))
+				lines := strings.Split(rendered, "\n")
+				for _, l := range lines {
+					if strings.TrimSpace(l) != "" {
+						sb.WriteString("    " + l + "\n")
 					}
-				} else {
-					conditions = append(conditions, fmt.Sprintf("net ~ [ %s+ ]", pfx))
-				}
-			}
-
-			if r.MatchASPath != "" {
-				conditions = append(conditions, fmt.Sprintf("bgp_path ~ [= %s =]", strings.TrimSpace(r.MatchASPath)))
-			}
-
-			if r.MatchCommunity != "" {
-				comm := strings.TrimSpace(r.MatchCommunity)
-				if strings.Contains(comm, ":") {
-					parts := strings.Split(comm, ":")
-					conditions = append(conditions, fmt.Sprintf("(%s, %s) ~ bgp_community", parts[0], parts[1]))
 				}
 			}
-
-			condStr := "true"
-			if len(conditions) > 0 {
-				condStr = strings.Join(conditions, " && ")
-			}
-
-			sb.WriteString(fmt.Sprintf("    if %s then {\n", condStr))
-			if r.SetLocalPref > 0 {
-				sb.WriteString(fmt.Sprintf("        bgp_local_pref = %d;\n", r.SetLocalPref))
-			}
-			if r.SetMetric > 0 {
-				sb.WriteString(fmt.Sprintf("        bgp_med = %d;\n", r.SetMetric))
-			}
-			if r.SetCommunity != "" {
-				comm := strings.TrimSpace(r.SetCommunity)
-				if strings.Contains(comm, ":") {
-					parts := strings.Split(comm, ":")
-					sb.WriteString(fmt.Sprintf("        bgp_community.add((%s, %s));\n", parts[0], parts[1]))
-				}
-			}
-			if r.SetNextHop != "" {
-				sb.WriteString(fmt.Sprintf("        bgp_next_hop = %s;\n", strings.TrimSpace(r.SetNextHop)))
-			}
-
-			if r.Action == "discard" || r.Action == "reject" {
-				sb.WriteString("        reject;\n")
-			} else {
-				sb.WriteString("        accept;\n")
-			}
-			sb.WriteString("    }\n")
 		}
 
-		sb.WriteString("    accept;\n")
+		// RouterOS v7 default chain policy: reject unmatched routes
+		sb.WriteString("    reject;\n")
 		sb.WriteString("}\n\n")
 	}
 
@@ -1632,6 +1628,394 @@ func (m *Manager) SetRoutingFilterState(id string, disabled bool) error {
 	return m.applyBirdConfig()
 }
 
+// translateRos7RuleToBird converts a RouterOS v7 rule script (or fallback fields) to valid BIRD 2 syntax
+func translateRos7RuleToBird(r *RoutingFilterRule) string {
+	script := strings.TrimSpace(r.Rule)
+	if script != "" {
+		s := script
+
+		// 1. Convert dst in A.B.C.D/M{L,H} -> net ~ [ A.B.C.D/M{L,H} ]
+		reDstRange := regexp.MustCompile(`dst\s+in\s+([0-9\.\/]+)\{([0-9]+),([0-9]+)\}`)
+		s = reDstRange.ReplaceAllString(s, "net ~ [ $1{$2,$3} ]")
+
+		// 2. Convert dst in A.B.C.D/M -> net ~ [ A.B.C.D/M+ ]
+		reDstIn := regexp.MustCompile(`dst\s+in\s+([0-9\.\/]+)`)
+		s = reDstIn.ReplaceAllString(s, "net ~ [ $1+ ]")
+
+		// 3. Convert dst == A.B.C.D/M -> net = A.B.C.D/M
+		reDstEq := regexp.MustCompile(`dst\s*==\s*([0-9\.\/]+)`)
+		s = reDstEq.ReplaceAllString(s, "net = $1")
+
+		// 4. Convert dst != A.B.C.D/M -> net != A.B.C.D/M
+		reDstNeq := regexp.MustCompile(`dst\s*!=\s*([0-9\.\/]+)`)
+		s = reDstNeq.ReplaceAllString(s, "net != $1")
+
+		// 5. Convert dst-len -> net.len
+		s = strings.ReplaceAll(s, "dst-len", "net.len")
+
+		// 6. Convert protocol matches
+		s = regexp.MustCompile(`protocol\s+bgp\b`).ReplaceAllString(s, "source = RTS_BGP")
+		s = regexp.MustCompile(`protocol\s+ospf\b`).ReplaceAllString(s, "source = RTS_OSPF")
+		s = regexp.MustCompile(`protocol\s+connected\b`).ReplaceAllString(s, "source = RTS_DEVICE")
+		s = regexp.MustCompile(`protocol\s+static\b`).ReplaceAllString(s, "source = RTS_STATIC")
+		s = regexp.MustCompile(`protocol\s+rip\b`).ReplaceAllString(s, "source = RTS_RIP")
+
+		// 7. Convert bgp-communities includes A:B -> (A, B) ~ bgp_community
+		reCommInc := regexp.MustCompile(`bgp-communities\s+includes\s+([0-9]+):([0-9]+)`)
+		s = reCommInc.ReplaceAllString(s, "($1, $2) ~ bgp_community")
+
+		// 8. Convert bgp-communities (in|equal-list) ListName -> bgp_community ~ comm_ListName
+		reCommList := regexp.MustCompile(`bgp-communities\s+(?:in|equal-list)\s+([A-Za-z0-9_]+)`)
+		s = reCommList.ReplaceAllString(s, "bgp_community ~ comm_$1")
+
+		// 9. Convert bgp-as-path in ListName -> bgp_path.last ~ aslist_ListName
+		reASList := regexp.MustCompile(`bgp-as-path\s+in\s+([A-Za-z0-9_]+)`)
+		s = reASList.ReplaceAllString(s, "bgp_path.last ~ aslist_$1")
+
+		// 10. Convert bgp-as-path "pattern" -> bgp_path ~ [= * pattern * =]
+		reASPattern := regexp.MustCompile(`bgp-as-path\s+"([^"]+)"`)
+		s = reASPattern.ReplaceAllStringFunc(s, func(match string) string {
+			sub := reASPattern.FindStringSubmatch(match)
+			if len(sub) == 2 {
+				pat := strings.Trim(sub[1], "^$")
+				return fmt.Sprintf("bgp_path ~ [= * %s * =]", pat)
+			}
+			return match
+		})
+
+		// 11. Convert actions:
+		reSetLP := regexp.MustCompile(`set\s+bgp-local-pref\s*=?\s*([0-9]+);?`)
+		s = reSetLP.ReplaceAllString(s, "bgp_local_pref = $1;")
+
+		reSetMed := regexp.MustCompile(`set\s+bgp-med\s*=?\s*([0-9]+);?`)
+		s = reSetMed.ReplaceAllString(s, "bgp_med = $1;")
+
+		reSetDist := regexp.MustCompile(`set\s+distance\s*=?\s*([0-9]+);?`)
+		s = reSetDist.ReplaceAllString(s, "preference = $1;")
+
+		reSetGW := regexp.MustCompile(`set\s+gw\s*=?\s*([0-9\.]+);?`)
+		s = reSetGW.ReplaceAllString(s, "gw = $1;")
+
+		reSetPrepend := regexp.MustCompile(`set\s+bgp-path-prepend\s*=?\s*([0-9]+);?`)
+		s = reSetPrepend.ReplaceAllStringFunc(s, func(match string) string {
+			sub := reSetPrepend.FindStringSubmatch(match)
+			if len(sub) == 2 {
+				n, _ := strconv.Atoi(sub[1])
+				if n > 0 && n <= 10 {
+					var b strings.Builder
+					for i := 0; i < n; i++ {
+						b.WriteString("bgp_path.prepend(bgp_path.first); ")
+					}
+					return b.String()
+				} else if n > 10 {
+					return fmt.Sprintf("bgp_path.prepend(%d);", n)
+				}
+			}
+			return match
+		})
+
+		reSetCommAdd := regexp.MustCompile(`set\s+bgp-communities\.add\s+([0-9]+):([0-9]+);?`)
+		s = reSetCommAdd.ReplaceAllString(s, "bgp_community.add(($1, $2));")
+
+		reSetCommDel := regexp.MustCompile(`set\s+bgp-communities\.delete\s+([0-9]+):([0-9]+);?`)
+		s = reSetCommDel.ReplaceAllString(s, "bgp_community.delete(($1, $2));")
+
+		// Ensure semicolons on accept and reject
+		s = regexp.MustCompile(`\baccept(?:\s*;)?`).ReplaceAllString(s, "accept;")
+		s = regexp.MustCompile(`\b(reject|discard)(?:\s*;)?`).ReplaceAllString(s, "reject;")
+
+		// Ensure "then" before opening brace of if statement
+		reIfThen := regexp.MustCompile(`\)\s*\{`)
+		s = reIfThen.ReplaceAllString(s, ") then {")
+		s = strings.ReplaceAll(s, ") then then {", ") then {")
+
+		return s
+	}
+
+	// Fallback for rules configured with structured UI form
+	var conditions []string
+
+	if r.Prefix != "" {
+		pfx := strings.TrimSpace(r.Prefix)
+		pfxLen := strings.TrimSpace(r.PrefixLen)
+		if pfxLen != "" {
+			if strings.Contains(pfxLen, "-") {
+				parts := strings.Split(pfxLen, "-")
+				if len(parts) == 2 {
+					conditions = append(conditions, fmt.Sprintf("net ~ [ %s{%s,%s} ]", pfx, strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])))
+				}
+			} else {
+				conditions = append(conditions, fmt.Sprintf("net ~ [ %s{%s,32} ]", pfx, pfxLen))
+			}
+		} else {
+			conditions = append(conditions, fmt.Sprintf("net ~ [ %s+ ]", pfx))
+		}
+	}
+
+	if r.MatchProtocol != "" && r.MatchProtocol != "any" {
+		switch strings.ToLower(r.MatchProtocol) {
+		case "bgp":
+			conditions = append(conditions, "source = RTS_BGP")
+		case "ospf":
+			conditions = append(conditions, "source = RTS_OSPF")
+		case "connected":
+			conditions = append(conditions, "source = RTS_DEVICE")
+		case "static":
+			conditions = append(conditions, "source = RTS_STATIC")
+		}
+	}
+
+	if r.MatchASPath != "" {
+		conditions = append(conditions, fmt.Sprintf("bgp_path ~ [= * %s * =]", strings.Trim(strings.TrimSpace(r.MatchASPath), "^$")))
+	}
+
+	if r.MatchCommunity != "" {
+		comm := strings.TrimSpace(r.MatchCommunity)
+		if strings.Contains(comm, ":") {
+			parts := strings.Split(comm, ":")
+			conditions = append(conditions, fmt.Sprintf("(%s, %s) ~ bgp_community", parts[0], parts[1]))
+		}
+	}
+
+	condStr := "true"
+	if len(conditions) > 0 {
+		condStr = strings.Join(conditions, " && ")
+	}
+
+	var sb strings.Builder
+	sb.WriteString(fmt.Sprintf("if %s then {\n", condStr))
+	if r.SetLocalPref > 0 {
+		sb.WriteString(fmt.Sprintf("    bgp_local_pref = %d;\n", r.SetLocalPref))
+	}
+	if r.SetMetric > 0 {
+		sb.WriteString(fmt.Sprintf("    bgp_med = %d;\n", r.SetMetric))
+	}
+	if r.SetASPrepend > 0 {
+		for i := 0; i < r.SetASPrepend; i++ {
+			sb.WriteString("    bgp_path.prepend(bgp_path.first);\n")
+		}
+	}
+	if r.SetCommunity != "" {
+		comm := strings.TrimSpace(r.SetCommunity)
+		if strings.Contains(comm, ":") {
+			parts := strings.Split(comm, ":")
+			sb.WriteString(fmt.Sprintf("    bgp_community.add((%s, %s));\n", parts[0], parts[1]))
+		}
+	}
+	if r.SetNextHop != "" {
+		sb.WriteString(fmt.Sprintf("    bgp_next_hop = %s;\n", strings.TrimSpace(r.SetNextHop)))
+	}
+	if r.SetDistance > 0 {
+		sb.WriteString(fmt.Sprintf("    preference = %d;\n", r.SetDistance))
+	}
+
+	if r.Action == "discard" || r.Action == "reject" {
+		sb.WriteString("    reject;\n")
+	} else {
+		sb.WriteString("    accept;\n")
+	}
+	sb.WriteString("}")
+	return sb.String()
+}
+
+// -------------------------------------------------------------
+// Select Rules (/routing filter select-rule)
+// -------------------------------------------------------------
+
+func (m *Manager) ListSelectRules() []RoutingFilterSelectRule {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
+	res := make([]RoutingFilterSelectRule, len(m.config.RoutingSelectRules))
+	copy(res, m.config.RoutingSelectRules)
+	return res
+}
+
+func (m *Manager) SaveSelectRule(rule *RoutingFilterSelectRule) error {
+	m.mu.Lock()
+	if rule.ID == "" {
+		rule.ID = fmt.Sprintf("sr-%d", time.Now().UnixNano()/1e6)
+	}
+	if rule.RuleOrder <= 0 {
+		rule.RuleOrder = (len(m.config.RoutingSelectRules) + 1) * 10
+	}
+
+	found := false
+	for i, r := range m.config.RoutingSelectRules {
+		if r.ID == rule.ID {
+			m.config.RoutingSelectRules[i] = *rule
+			found = true
+			break
+		}
+	}
+	if !found {
+		m.config.RoutingSelectRules = append(m.config.RoutingSelectRules, *rule)
+	}
+	_ = m.save()
+	m.mu.Unlock()
+
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) DeleteSelectRule(id string) error {
+	m.mu.Lock()
+	newRules := make([]RoutingFilterSelectRule, 0, len(m.config.RoutingSelectRules))
+	for _, r := range m.config.RoutingSelectRules {
+		if r.ID != id {
+			newRules = append(newRules, r)
+		}
+	}
+	m.config.RoutingSelectRules = newRules
+	_ = m.save()
+	m.mu.Unlock()
+
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) SetSelectRuleState(id string, disabled bool) error {
+	m.mu.Lock()
+	for i := range m.config.RoutingSelectRules {
+		if m.config.RoutingSelectRules[i].ID == id {
+			m.config.RoutingSelectRules[i].Disabled = disabled
+			break
+		}
+	}
+	_ = m.save()
+	m.mu.Unlock()
+
+	return m.applyBirdConfig()
+}
+
+// -------------------------------------------------------------
+// Community Lists (/routing filter community-list)
+// -------------------------------------------------------------
+
+func (m *Manager) ListCommunityLists() []RoutingFilterCommunityList {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
+	res := make([]RoutingFilterCommunityList, len(m.config.RoutingCommunityLists))
+	copy(res, m.config.RoutingCommunityLists)
+	return res
+}
+
+func (m *Manager) SaveCommunityList(cl *RoutingFilterCommunityList) error {
+	m.mu.Lock()
+	if cl.ID == "" {
+		cl.ID = fmt.Sprintf("cl-%d", time.Now().UnixNano()/1e6)
+	}
+
+	found := false
+	for i, r := range m.config.RoutingCommunityLists {
+		if r.ID == cl.ID {
+			m.config.RoutingCommunityLists[i] = *cl
+			found = true
+			break
+		}
+	}
+	if !found {
+		m.config.RoutingCommunityLists = append(m.config.RoutingCommunityLists, *cl)
+	}
+	_ = m.save()
+	m.mu.Unlock()
+
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) DeleteCommunityList(id string) error {
+	m.mu.Lock()
+	newLists := make([]RoutingFilterCommunityList, 0, len(m.config.RoutingCommunityLists))
+	for _, r := range m.config.RoutingCommunityLists {
+		if r.ID != id {
+			newLists = append(newLists, r)
+		}
+	}
+	m.config.RoutingCommunityLists = newLists
+	_ = m.save()
+	m.mu.Unlock()
+
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) SetCommunityListState(id string, disabled bool) error {
+	m.mu.Lock()
+	for i := range m.config.RoutingCommunityLists {
+		if m.config.RoutingCommunityLists[i].ID == id {
+			m.config.RoutingCommunityLists[i].Disabled = disabled
+			break
+		}
+	}
+	_ = m.save()
+	m.mu.Unlock()
+
+	return m.applyBirdConfig()
+}
+
+// -------------------------------------------------------------
+// AS Lists (/routing filter num-list)
+// -------------------------------------------------------------
+
+func (m *Manager) ListASLists() []RoutingFilterASList {
+	m.mu.RLock()
+	defer m.mu.RUnlock()
+
+	res := make([]RoutingFilterASList, len(m.config.RoutingASLists))
+	copy(res, m.config.RoutingASLists)
+	return res
+}
+
+func (m *Manager) SaveASList(al *RoutingFilterASList) error {
+	m.mu.Lock()
+	if al.ID == "" {
+		al.ID = fmt.Sprintf("al-%d", time.Now().UnixNano()/1e6)
+	}
+
+	found := false
+	for i, r := range m.config.RoutingASLists {
+		if r.ID == al.ID {
+			m.config.RoutingASLists[i] = *al
+			found = true
+			break
+		}
+	}
+	if !found {
+		m.config.RoutingASLists = append(m.config.RoutingASLists, *al)
+	}
+	_ = m.save()
+	m.mu.Unlock()
+
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) DeleteASList(id string) error {
+	m.mu.Lock()
+	newLists := make([]RoutingFilterASList, 0, len(m.config.RoutingASLists))
+	for _, r := range m.config.RoutingASLists {
+		if r.ID != id {
+			newLists = append(newLists, r)
+		}
+	}
+	m.config.RoutingASLists = newLists
+	_ = m.save()
+	m.mu.Unlock()
+
+	return m.applyBirdConfig()
+}
+
+func (m *Manager) SetASListState(id string, disabled bool) error {
+	m.mu.Lock()
+	for i := range m.config.RoutingASLists {
+		if m.config.RoutingASLists[i].ID == id {
+			m.config.RoutingASLists[i].Disabled = disabled
+			break
+		}
+	}
+	_ = m.save()
+	m.mu.Unlock()
+
+	return m.applyBirdConfig()
+}
+
 // -------------------------------------------------------------
 // Summary Telemetry
 // -------------------------------------------------------------

+ 44 - 11
internal/dynamicrouting/types.go

@@ -203,11 +203,12 @@ type OSPFRoute struct {
 	Interface string `json:"interface"`
 }
 
-// RoutingFilterRule represents a route filter rule (/routing filter)
+// RoutingFilterRule represents a route filter rule (/routing filter rule)
 type RoutingFilterRule struct {
 	ID             string `json:"id"`
 	Chain          string `json:"chain"`                   // e.g. "bgp-in", "bgp-out", "isp-in"
 	RuleOrder      int    `json:"rule_order"`              // e.g. 10, 20, 30... Sequence number
+	Rule           string `json:"rule"`                    // RouterOS v7 rule script e.g. "if (dst in 10.0.0.0/8) { set bgp-local-pref 150; accept; }"
 	Action         string `json:"action"`                  // "accept" (permit), "discard" (deny), "reject", "passthrough"
 	Prefix         string `json:"prefix,omitempty"`         // CIDR e.g. "10.0.0.0/8" or "0.0.0.0/0"
 	PrefixLen      string `json:"prefix_len,omitempty"`     // e.g. "24-32", "ge 24 le 32", "24"
@@ -224,6 +225,35 @@ type RoutingFilterRule struct {
 	Comment        string `json:"comment,omitempty"`
 }
 
+// RoutingFilterSelectRule represents a route selection rule (/routing filter select-rule)
+type RoutingFilterSelectRule struct {
+	ID        string `json:"id"`
+	Chain     string `json:"chain"`
+	RuleOrder int    `json:"rule_order"`
+	Rule      string `json:"rule"`
+	Disabled  bool   `json:"disabled"`
+	Comment   string `json:"comment,omitempty"`
+}
+
+// RoutingFilterCommunityList represents a named community set (/routing filter community-list)
+type RoutingFilterCommunityList struct {
+	ID          string   `json:"id"`
+	Name        string   `json:"name"`
+	Communities []string `json:"communities"`
+	Regexp      string   `json:"regexp,omitempty"`
+	Disabled    bool     `json:"disabled"`
+	Comment     string   `json:"comment,omitempty"`
+}
+
+// RoutingFilterASList represents a named AS number list (/routing filter num-list)
+type RoutingFilterASList struct {
+	ID       string `json:"id"`
+	Name     string `json:"name"`
+	Range    string `json:"range"`
+	Disabled bool   `json:"disabled"`
+	Comment  string `json:"comment,omitempty"`
+}
+
 // DynamicRoutingSummary aggregates status for dashboard & badges
 type DynamicRoutingSummary struct {
 	BGPEnabled          bool `json:"bgp_enabled"`
@@ -239,14 +269,17 @@ type DynamicRoutingSummary struct {
 
 // DynamicConfig stores all dynamic routing data on disk
 type DynamicConfig struct {
-	BGPInstance    BGPInstance          `json:"bgp_instance"`
-	BGPPeers       []BGPPeer            `json:"bgp_peers"`
-	BGPNetworks    []BGPNetwork         `json:"bgp_networks"`
-	OSPFInstances  []OSPFInstance       `json:"ospf_instances"`
-	OSPFInstance   OSPFInstance         `json:"ospf_instance"` // Backward compatibility
-	OSPFAreas      []OSPFArea           `json:"ospf_areas"`
-	OSPFAreaRanges []OSPFAreaRange      `json:"ospf_area_ranges"`
-	OSPFInterfaces []OSPFInterface      `json:"ospf_interfaces"`
-	OSPFStaticNbrs []OSPFStaticNeighbor `json:"ospf_static_nbrs"`
-	RoutingFilters []RoutingFilterRule  `json:"routing_filters"`
+	BGPInstance           BGPInstance                  `json:"bgp_instance"`
+	BGPPeers              []BGPPeer                    `json:"bgp_peers"`
+	BGPNetworks           []BGPNetwork                 `json:"bgp_networks"`
+	OSPFInstances         []OSPFInstance               `json:"ospf_instances"`
+	OSPFInstance          OSPFInstance                 `json:"ospf_instance"` // Backward compatibility
+	OSPFAreas             []OSPFArea                   `json:"ospf_areas"`
+	OSPFAreaRanges        []OSPFAreaRange              `json:"ospf_area_ranges"`
+	OSPFInterfaces        []OSPFInterface              `json:"ospf_interfaces"`
+	OSPFStaticNbrs        []OSPFStaticNeighbor         `json:"ospf_static_nbrs"`
+	RoutingFilters        []RoutingFilterRule          `json:"routing_filters"`
+	RoutingSelectRules    []RoutingFilterSelectRule    `json:"routing_select_rules"`
+	RoutingCommunityLists []RoutingFilterCommunityList `json:"routing_community_lists"`
+	RoutingASLists        []RoutingFilterASList        `json:"routing_as_lists"`
 }

+ 863 - 128
internal/web/static/app.js

@@ -6957,7 +6957,8 @@ async function loadDynamicRouting(showToastMsg = false) {
 
     const [
       sum, bgpInst, bgpPeers, bgpNets, bgpRoutes,
-      ospfInsts, ospfAreas, ospfRanges, ospfTemplates, ospfLiveIfaces, ospfStaticNbrs, ospfNbrs, ospfLsa, filters
+      ospfInsts, ospfAreas, ospfRanges, ospfTemplates, ospfLiveIfaces, ospfStaticNbrs, ospfNbrs, ospfLsa, filters,
+      selectRules, commLists, asLists
     ] = await Promise.all([
       fetchSafeJson(`${API_BASE}/routing/dynamic/summary`),
       fetchSafeJson(`${API_BASE}/routing/bgp/instance`),
@@ -6972,7 +6973,10 @@ async function loadDynamicRouting(showToastMsg = false) {
       fetchSafeJson(`${API_BASE}/routing/ospf/static-neighbors`),
       fetchSafeJson(`${API_BASE}/routing/ospf/neighbors`),
       fetchSafeJson(`${API_BASE}/routing/ospf/lsa`),
-      fetchSafeJson(`${API_BASE}/routing/filters`)
+      fetchSafeJson(`${API_BASE}/routing/filters`),
+      fetchSafeJson(`${API_BASE}/routing/filters/select-rules`),
+      fetchSafeJson(`${API_BASE}/routing/filters/community-lists`),
+      fetchSafeJson(`${API_BASE}/routing/filters/as-lists`)
     ]);
 
     if (sum) state.dynSummary = sum;
@@ -6989,6 +6993,9 @@ async function loadDynamicRouting(showToastMsg = false) {
     if (ospfNbrs) state.ospfNeighbors = ospfNbrs;
     if (ospfLsa) state.ospfLSA = ospfLsa;
     if (filters) state.routingFilters = filters;
+    if (selectRules) state.routingSelectRules = selectRules;
+    if (commLists) state.routingCommunityLists = commLists;
+    if (asLists) state.routingASLists = asLists;
 
     try { renderBgpStatusPill(); } catch(e) { console.error('renderBgpStatusPill error:', e); }
     try { renderBgpPeers(); } catch(e) { console.error('renderBgpPeers error:', e); }
@@ -9621,24 +9628,147 @@ function populateBridgePortCandidateSelect(selectId, currentVal = '') {
 }
 
 // -------------------------------------------------------------
-// Routing Filters Management (/routing filter)
+// RouterOS v7 Routing Filters Management (/routing filter)
 // -------------------------------------------------------------
 
+if (!state.activeRoutingFilterTab) state.activeRoutingFilterTab = 'rules';
+if (!state.selectedFilterRow) state.selectedFilterRow = {};
+if (!state.rfSort) state.rfSort = {};
+
+function switchRoutingFilterTab(tab) {
+  state.activeRoutingFilterTab = tab;
+  const tabs = ['rules', 'select-rules', 'community-lists', 'as-lists'];
+
+  tabs.forEach(t => {
+    const btn = document.getElementById(`tab-rf-${t}`);
+    const content = document.getElementById(`tab-content-rf-${t}`);
+    if (btn) {
+      if (t === tab) {
+        btn.className = 'px-3 py-1 text-xs font-semibold rounded bg-emerald-600/20 text-emerald-300 border border-emerald-500/30 transition shadow-sm';
+      } else {
+        btn.className = 'px-3 py-1 text-xs font-semibold rounded text-slate-400 hover:text-white hover:bg-slate-800 transition';
+      }
+    }
+    if (content) {
+      if (t === tab) content.classList.remove('hidden');
+      else content.classList.add('hidden');
+    }
+  });
+
+  const chainContainer = document.getElementById('rf-chain-filter-container');
+  if (chainContainer) {
+    if (tab === 'rules' || tab === 'select-rules') chainContainer.classList.remove('hidden');
+    else chainContainer.classList.add('hidden');
+  }
+
+  renderRoutingFilters();
+}
+
+function selectFilterRow(tab, id) {
+  if (state.selectedFilterRow[tab] === id) {
+    delete state.selectedFilterRow[tab];
+  } else {
+    state.selectedFilterRow[tab] = id;
+  }
+  if (tab === 'rules') renderRoutingFilterRules();
+  else if (tab === 'select-rules') renderRoutingFilterSelectRules();
+  else if (tab === 'community-lists') renderRoutingFilterCommunityLists();
+  else if (tab === 'as-lists') renderRoutingFilterASLists();
+}
+
+function openRoutingFilterNewModal() {
+  const tab = state.activeRoutingFilterTab || 'rules';
+  if (tab === 'rules') openAddRoutingFilterModal();
+  else if (tab === 'select-rules') openAddSelectRuleModal();
+  else if (tab === 'community-lists') openAddCommunityListModal();
+  else if (tab === 'as-lists') openAddASListModal();
+}
+
+function toggleSelectedRoutingFilter(disabled) {
+  const tab = state.activeRoutingFilterTab || 'rules';
+  const id = state.selectedFilterRow[tab];
+  if (!id) {
+    showToast('Select Row', 'Please click on a row to select it first', 'info');
+    return;
+  }
+  if (tab === 'rules') toggleRoutingFilterState(id, disabled);
+  else if (tab === 'select-rules') toggleSelectRuleState(id, disabled);
+  else if (tab === 'community-lists') toggleCommunityListState(id, disabled);
+  else if (tab === 'as-lists') toggleASListState(id, disabled);
+}
+
+function removeSelectedRoutingFilter() {
+  const tab = state.activeRoutingFilterTab || 'rules';
+  const id = state.selectedFilterRow[tab];
+  if (!id) {
+    showToast('Select Row', 'Please click on a row to select it first', 'info');
+    return;
+  }
+  if (tab === 'rules') deleteRoutingFilter(id);
+  else if (tab === 'select-rules') deleteSelectRule(id);
+  else if (tab === 'community-lists') deleteCommunityList(id);
+  else if (tab === 'as-lists') deleteASList(id);
+}
+
+function searchRoutingFilters(q) {
+  state.rfSearchQuery = (q || '').trim().toLowerCase();
+  renderRoutingFilters();
+}
+
+function sortFilterTable(tab, col) {
+  if (!state.rfSort) state.rfSort = {};
+  if (!state.rfSort[tab]) state.rfSort[tab] = { col: col, asc: true };
+  if (state.rfSort[tab].col === col) {
+    state.rfSort[tab].asc = !state.rfSort[tab].asc;
+  } else {
+    state.rfSort[tab].col = col;
+    state.rfSort[tab].asc = true;
+  }
+  const asc = state.rfSort[tab].asc;
+
+  let list = [];
+  if (tab === 'rules') list = state.routingFilters || [];
+  else if (tab === 'select-rules') list = state.routingSelectRules || [];
+  else if (tab === 'community-lists') list = state.routingCommunityLists || [];
+  else if (tab === 'as-lists') list = state.routingASLists || [];
+
+  list.sort((a, b) => {
+    let valA = a[col] ?? '';
+    let valB = b[col] ?? '';
+    if (col === 'flags') {
+      valA = a.disabled ? 'X' : 'A';
+      valB = b.disabled ? 'X' : 'A';
+    }
+    if (typeof valA === 'number' && typeof valB === 'number') {
+      return asc ? valA - valB : valB - valA;
+    }
+    return asc ? String(valA).localeCompare(String(valB)) : String(valB).localeCompare(String(valA));
+  });
+
+  const allCols = ['flags', 'rule_order', 'chain', 'rule', 'name', 'communities', 'range', 'regexp', 'comment'];
+  allCols.forEach(c => {
+    const el = document.getElementById(`sort-icon-${tab}-${c}`);
+    if (el) el.innerText = (c === col) ? (asc ? '▲' : '▼') : '';
+  });
+
+  if (tab === 'rules') renderRoutingFilterRules();
+  else if (tab === 'select-rules') renderRoutingFilterSelectRules();
+  else if (tab === 'community-lists') renderRoutingFilterCommunityLists();
+  else if (tab === 'as-lists') renderRoutingFilterASLists();
+}
+
 function renderRoutingFilters() {
-  const tbody = document.getElementById('routing-filters-table-body');
-  if (!tbody) return;
-  const empty = document.getElementById('routing-filters-empty');
-  const countBadge = document.getElementById('routing-filters-count-badge');
   const navBadge = document.getElementById('badge-filters-count');
-  const chainSelect = document.getElementById('routing-filter-chain-select');
-
   const allRules = state.routingFilters || [];
   if (navBadge) navBadge.innerText = allRules.length;
 
-  // Populate chain filter select options dynamically
+  // Populate chain filter select options and datalist
+  const chainSelect = document.getElementById('routing-filter-chain-select');
+  const chainDatalist = document.getElementById('dl-routing-chains');
+  const uniqueChains = Array.from(new Set(allRules.map(r => r.chain).filter(Boolean))).sort();
+
   if (chainSelect) {
     const currentVal = state.filterChainSelected || 'all';
-    const uniqueChains = Array.from(new Set(allRules.map(r => r.chain).filter(Boolean))).sort();
     chainSelect.innerHTML = `<option value="all">All Chains (${allRules.length})</option>` +
       uniqueChains.map(ch => {
         const count = allRules.filter(r => r.chain === ch).length;
@@ -9647,105 +9777,168 @@ function renderRoutingFilters() {
     chainSelect.value = currentVal;
   }
 
-  // Filter rules by selected chain
-  const filteredRules = (state.filterChainSelected && state.filterChainSelected !== 'all')
-    ? allRules.filter(r => r.chain === state.filterChainSelected)
-    : allRules.slice();
+  if (chainDatalist) {
+    chainDatalist.innerHTML = uniqueChains.map(ch => `<option value="${escapeHtml(ch)}">`).join('');
+  }
 
-  // Sort by chain then rule_order
-  filteredRules.sort((a, b) => {
-    if (a.chain !== b.chain) return (a.chain || '').localeCompare(b.chain || '');
-    return (a.rule_order || 0) - (b.rule_order || 0);
-  });
+  renderRoutingFilterRules();
+  renderRoutingFilterSelectRules();
+  renderRoutingFilterCommunityLists();
+  renderRoutingFilterASLists();
+}
+
+function filterRoutingFiltersByChain(chain) {
+  state.filterChainSelected = chain;
+  renderRoutingFilters();
+}
+
+// -------------------------------------------------------------
+// TAB 1: RULES (/routing filter rule)
+// -------------------------------------------------------------
+
+function renderRoutingFilterRules() {
+  const tbody = document.getElementById('routing-filter-rules-tbody');
+  if (!tbody) return;
+  const empty = document.getElementById('rf-rules-empty');
+  const statusCount = document.getElementById('rf-status-count');
+
+  let list = (state.routingFilters || []).slice();
+
+  if (state.filterChainSelected && state.filterChainSelected !== 'all') {
+    list = list.filter(r => r.chain === state.filterChainSelected);
+  }
+
+  const q = state.rfSearchQuery;
+  if (q) {
+    list = list.filter(r =>
+      (r.chain && r.chain.toLowerCase().includes(q)) ||
+      (r.rule && r.rule.toLowerCase().includes(q)) ||
+      (r.prefix && r.prefix.toLowerCase().includes(q)) ||
+      (r.comment && r.comment.toLowerCase().includes(q))
+    );
+  }
+
+  if (!state.rfSort?.['rules']) {
+    list.sort((a, b) => {
+      if (a.chain !== b.chain) return (a.chain || '').localeCompare(b.chain || '');
+      return (a.rule_order || 0) - (b.rule_order || 0);
+    });
+  }
 
-  if (countBadge) countBadge.innerText = `${filteredRules.length} Rules`;
+  if (statusCount && (state.activeRoutingFilterTab === 'rules' || !state.activeRoutingFilterTab)) {
+    statusCount.innerText = `${list.length} item${list.length === 1 ? '' : 's'}`;
+  }
 
-  if (filteredRules.length === 0) {
+  if (list.length === 0) {
     tbody.innerHTML = '';
     if (empty) empty.classList.remove('hidden');
     return;
   }
   if (empty) empty.classList.add('hidden');
 
-  tbody.innerHTML = filteredRules.map(r => {
+  tbody.innerHTML = list.map(r => {
+    const isSelected = state.selectedFilterRow?.['rules'] === r.id;
     const flagA = !r.disabled ? '<span class="text-cyan-400 font-bold" title="Active">A</span>' : '';
-    const flagX = r.disabled ? '<span class="text-rose-400 font-bold ml-1" title="Disabled">X</span>' : '';
-
-    let actionBadge = '';
-    if (r.action === 'accept') {
-      actionBadge = '<span class="px-2 py-0.5 rounded text-[10px] font-sans font-semibold bg-emerald-500/10 text-emerald-400 border border-emerald-500/20">accept</span>';
-    } else if (r.action === 'discard') {
-      actionBadge = '<span class="px-2 py-0.5 rounded text-[10px] font-sans font-semibold bg-rose-500/10 text-rose-400 border border-rose-500/20">discard</span>';
-    } else if (r.action === 'reject') {
-      actionBadge = '<span class="px-2 py-0.5 rounded text-[10px] font-sans font-semibold bg-red-950 text-red-300 border border-red-800/40">reject</span>';
-    } else {
-      actionBadge = `<span class="px-2 py-0.5 rounded text-[10px] font-sans font-semibold bg-blue-500/10 text-blue-400 border border-blue-500/20">${escapeHtml(r.action || 'accept')}</span>`;
-    }
+    const flagX = r.disabled ? '<span class="text-rose-400 font-bold" title="Disabled">X</span>' : '';
+    const flagC = r.comment ? '<span class="text-amber-400 font-bold ml-1" title="Comment">C</span>' : '';
 
-    // Prefix match
-    let matchPfx = '';
-    if (r.prefix) {
-      matchPfx = `<span class="font-bold text-white">${escapeHtml(r.prefix)}</span>`;
-      if (r.prefix_len) {
-        matchPfx += ` <span class="text-slate-400 text-[11px] font-normal">[${escapeHtml(r.prefix_len)}]</span>`;
-      }
+    let ruleDisplay = '';
+    if (r.rule && r.rule.trim() !== '') {
+      ruleDisplay = `<span class="text-emerald-400 font-mono font-medium">${escapeHtml(r.rule)}</span>`;
     } else {
-      matchPfx = '<span class="text-slate-500 italic">any prefix</span>';
+      let conds = [];
+      if (r.prefix) conds.push(`dst in ${escapeHtml(r.prefix)}`);
+      if (r.match_protocol && r.match_protocol !== 'any') conds.push(`protocol ${escapeHtml(r.match_protocol)}`);
+      let act = r.action || 'accept';
+      ruleDisplay = `<span class="text-slate-300 font-mono">if (${conds.length ? conds.join(' && ') : 'true'}) { ${escapeHtml(act)}; }</span>`;
     }
 
-    // Other matches
-    const matchExtra = [];
-    if (r.match_protocol && r.match_protocol !== 'any') {
-      matchExtra.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-slate-800 text-slate-300 border border-slate-700">proto:${escapeHtml(r.match_protocol)}</span>`);
-    }
-    if (r.match_as_path) {
-      matchExtra.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-purple-950 text-purple-300 border border-purple-800/40 font-mono">as-path:${escapeHtml(r.match_as_path)}</span>`);
-    }
-    if (r.match_community) {
-      matchExtra.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-indigo-950 text-indigo-300 border border-indigo-800/40 font-mono">comm:${escapeHtml(r.match_community)}</span>`);
-    }
-    const matchExtraHtml = matchExtra.length > 0 ? matchExtra.join(' ') : '<span class="text-slate-600">-</span>';
+    return `
+      <tr onclick="selectFilterRow('rules', '${r.id}')" class="cursor-pointer transition select-none ${isSelected ? 'bg-emerald-950/40 ring-1 ring-emerald-500/40' : 'hover:bg-slate-800/40'}">
+        <td class="py-2 px-2 text-center tracking-wider">${flagA}${flagX}${flagC}</td>
+        <td class="py-2 px-2 font-mono text-slate-400 font-bold">${r.rule_order || 10}</td>
+        <td class="py-2 px-3">
+          <span class="px-2 py-0.5 rounded text-[11px] font-mono bg-slate-800 text-cyan-300 border border-slate-700 font-semibold">${escapeHtml(r.chain)}</span>
+        </td>
+        <td class="py-2 px-3 break-all max-w-xl">${ruleDisplay}</td>
+        <td class="py-2 px-3 text-slate-400 font-sans truncate max-w-xs">${escapeHtml(r.comment || '')}</td>
+        <td class="py-2 px-3 text-right space-x-1 whitespace-nowrap font-sans" onclick="event.stopPropagation()">
+          <button onclick="moveRoutingFilterOrder('${r.id}', -1)" title="Move Up" class="px-1.5 py-0.5 rounded text-[10px] bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700">▲</button>
+          <button onclick="moveRoutingFilterOrder('${r.id}', 1)" title="Move Down" class="px-1.5 py-0.5 rounded text-[10px] bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700">▼</button>
+          <button onclick="toggleRoutingFilterState('${r.id}', ${!r.disabled})" class="px-2 py-0.5 rounded text-[10px] ${r.disabled ? 'bg-emerald-950 hover:bg-emerald-900 text-emerald-400 border border-emerald-800/50' : 'bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700'}">
+            ${r.disabled ? 'Enable' : 'Disable'}
+          </button>
+          <button onclick="openEditRoutingFilterModal('${r.id}')" class="px-2 py-0.5 rounded text-[10px] bg-slate-800 hover:bg-slate-700 text-emerald-300 border border-slate-700">
+            Edit
+          </button>
+          <button onclick="deleteRoutingFilter('${r.id}')" class="px-2 py-0.5 rounded text-[10px] bg-rose-950/60 hover:bg-rose-900 text-rose-300 border border-rose-900/50">
+            ✕
+          </button>
+        </td>
+      </tr>
+    `;
+  }).join('');
+}
 
-    // Set modifications
-    const setBadges = [];
-    if (r.set_local_pref > 0) {
-      setBadges.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-cyan-950 text-cyan-300 border border-cyan-800/40">loc-pref: ${r.set_local_pref}</span>`);
-    }
-    if (r.set_metric > 0) {
-      setBadges.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-amber-950 text-amber-300 border border-amber-800/40">med: ${r.set_metric}</span>`);
-    }
-    if (r.set_as_prepend > 0) {
-      setBadges.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-purple-950 text-purple-300 border border-purple-800/40">prepend: +${r.set_as_prepend}</span>`);
-    }
-    if (r.set_community) {
-      setBadges.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-indigo-950 text-indigo-300 border border-indigo-800/40">comm: ${escapeHtml(r.set_community)}</span>`);
-    }
-    if (r.set_next_hop) {
-      setBadges.push(`<span class="px-1.5 py-0.5 rounded text-[10px] bg-blue-950 text-blue-300 border border-blue-800/40">nh: ${escapeHtml(r.set_next_hop)}</span>`);
-    }
-    const setHtml = setBadges.length > 0 ? setBadges.join(' ') : '<span class="text-slate-600">-</span>';
+// -------------------------------------------------------------
+// TAB 2: SELECT RULES (/routing filter select-rule)
+// -------------------------------------------------------------
+
+function renderRoutingFilterSelectRules() {
+  const tbody = document.getElementById('routing-filter-select-rules-tbody');
+  if (!tbody) return;
+  const empty = document.getElementById('rf-select-rules-empty');
+  const statusCount = document.getElementById('rf-status-count');
+
+  let list = (state.routingSelectRules || []).slice();
+
+  if (state.filterChainSelected && state.filterChainSelected !== 'all') {
+    list = list.filter(r => r.chain === state.filterChainSelected);
+  }
+
+  const q = state.rfSearchQuery;
+  if (q) {
+    list = list.filter(r =>
+      (r.chain && r.chain.toLowerCase().includes(q)) ||
+      (r.rule && r.rule.toLowerCase().includes(q)) ||
+      (r.comment && r.comment.toLowerCase().includes(q))
+    );
+  }
+
+  if (statusCount && state.activeRoutingFilterTab === 'select-rules') {
+    statusCount.innerText = `${list.length} item${list.length === 1 ? '' : 's'}`;
+  }
+
+  if (list.length === 0) {
+    tbody.innerHTML = '';
+    if (empty) empty.classList.remove('hidden');
+    return;
+  }
+  if (empty) empty.classList.add('hidden');
+
+  tbody.innerHTML = list.map(r => {
+    const isSelected = state.selectedFilterRow?.['select-rules'] === r.id;
+    const flagA = !r.disabled ? '<span class="text-cyan-400 font-bold">A</span>' : '';
+    const flagX = r.disabled ? '<span class="text-rose-400 font-bold">X</span>' : '';
 
     return `
-      <tr class="hover:bg-slate-800/40 transition">
-        <td class="py-2.5 px-3 text-center tracking-wider">${flagA}${flagX}</td>
-        <td class="py-2.5 px-3 font-mono text-slate-400 font-bold">${r.rule_order || 10}</td>
-        <td class="py-2.5 px-3">
+      <tr onclick="selectFilterRow('select-rules', '${r.id}')" class="cursor-pointer transition select-none ${isSelected ? 'bg-emerald-950/40 ring-1 ring-emerald-500/40' : 'hover:bg-slate-800/40'}">
+        <td class="py-2 px-2 text-center tracking-wider">${flagA}${flagX}</td>
+        <td class="py-2 px-2 font-mono text-slate-400 font-bold">${r.rule_order || 10}</td>
+        <td class="py-2 px-3">
           <span class="px-2 py-0.5 rounded text-[11px] font-mono bg-slate-800 text-cyan-300 border border-slate-700 font-semibold">${escapeHtml(r.chain)}</span>
         </td>
-        <td class="py-2.5 px-3">${actionBadge}</td>
-        <td class="py-2.5 px-3 font-mono">${matchPfx}</td>
-        <td class="py-2.5 px-3 space-x-1">${matchExtraHtml}</td>
-        <td class="py-2.5 px-3 space-x-1 font-mono">${setHtml}</td>
-        <td class="py-2.5 px-3 text-slate-400 font-sans truncate max-w-xs">${escapeHtml(r.comment || '')}</td>
-        <td class="py-2.5 px-3 text-right space-x-1.5 whitespace-nowrap font-sans">
-          <button onclick="toggleRoutingFilterState('${r.id}', ${!r.disabled})" class="px-2 py-1 rounded text-[11px] ${r.disabled ? 'bg-emerald-950 hover:bg-emerald-900 text-emerald-400 border border-emerald-800/50' : 'bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700'}">
+        <td class="py-2 px-3 break-all max-w-xl font-mono text-emerald-400">${escapeHtml(r.rule || '')}</td>
+        <td class="py-2 px-3 text-slate-400 font-sans truncate max-w-xs">${escapeHtml(r.comment || '')}</td>
+        <td class="py-2 px-3 text-right space-x-1 whitespace-nowrap font-sans" onclick="event.stopPropagation()">
+          <button onclick="toggleSelectRuleState('${r.id}', ${!r.disabled})" class="px-2 py-0.5 rounded text-[10px] ${r.disabled ? 'bg-emerald-950 hover:bg-emerald-900 text-emerald-400 border border-emerald-800/50' : 'bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700'}">
             ${r.disabled ? 'Enable' : 'Disable'}
           </button>
-          <button onclick="openEditRoutingFilterModal('${r.id}')" class="px-2 py-1 rounded text-[11px] bg-slate-800 hover:bg-slate-700 text-emerald-300 border border-slate-700">
+          <button onclick="openEditSelectRuleModal('${r.id}')" class="px-2 py-0.5 rounded text-[10px] bg-slate-800 hover:bg-slate-700 text-emerald-300 border border-slate-700">
             Edit
           </button>
-          <button onclick="deleteRoutingFilter('${r.id}')" class="px-2 py-1 rounded text-[11px] bg-rose-950/60 hover:bg-rose-900 text-rose-300 border border-rose-900/50">
-            Delete
+          <button onclick="deleteSelectRule('${r.id}')" class="px-2 py-0.5 rounded text-[10px] bg-rose-950/60 hover:bg-rose-900 text-rose-300 border border-rose-900/50">
+            ✕
           </button>
         </td>
       </tr>
@@ -9753,11 +9946,224 @@ function renderRoutingFilters() {
   }).join('');
 }
 
-function filterRoutingFiltersByChain(chain) {
-  state.filterChainSelected = chain;
-  renderRoutingFilters();
+// -------------------------------------------------------------
+// TAB 3: COMMUNITY LISTS (/routing filter community-list)
+// -------------------------------------------------------------
+
+function renderRoutingFilterCommunityLists() {
+  const tbody = document.getElementById('routing-filter-comm-lists-tbody');
+  if (!tbody) return;
+  const empty = document.getElementById('rf-comm-lists-empty');
+  const statusCount = document.getElementById('rf-status-count');
+
+  let list = (state.routingCommunityLists || []).slice();
+
+  const q = state.rfSearchQuery;
+  if (q) {
+    list = list.filter(cl =>
+      (cl.name && cl.name.toLowerCase().includes(q)) ||
+      (cl.comment && cl.comment.toLowerCase().includes(q)) ||
+      (cl.communities && cl.communities.some(c => c.toLowerCase().includes(q)))
+    );
+  }
+
+  if (statusCount && state.activeRoutingFilterTab === 'community-lists') {
+    statusCount.innerText = `${list.length} item${list.length === 1 ? '' : 's'}`;
+  }
+
+  if (list.length === 0) {
+    tbody.innerHTML = '';
+    if (empty) empty.classList.remove('hidden');
+    return;
+  }
+  if (empty) empty.classList.add('hidden');
+
+  tbody.innerHTML = list.map(cl => {
+    const isSelected = state.selectedFilterRow?.['community-lists'] === cl.id;
+    const flagA = !cl.disabled ? '<span class="text-cyan-400 font-bold">A</span>' : '';
+    const flagX = cl.disabled ? '<span class="text-rose-400 font-bold">X</span>' : '';
+
+    const commBadges = (cl.communities || []).map(c =>
+      `<span class="px-1.5 py-0.5 rounded text-[10px] bg-indigo-950 text-indigo-300 border border-indigo-800/40 font-mono">${escapeHtml(c)}</span>`
+    ).join(' ');
+
+    return `
+      <tr onclick="selectFilterRow('community-lists', '${cl.id}')" class="cursor-pointer transition select-none ${isSelected ? 'bg-emerald-950/40 ring-1 ring-emerald-500/40' : 'hover:bg-slate-800/40'}">
+        <td class="py-2 px-2 text-center tracking-wider">${flagA}${flagX}</td>
+        <td class="py-2 px-3 font-mono font-bold text-white">${escapeHtml(cl.name)}</td>
+        <td class="py-2 px-3 space-x-1">${commBadges || '<span class="text-slate-600">-</span>'}</td>
+        <td class="py-2 px-3 font-mono text-slate-400">${escapeHtml(cl.regexp || '-')}</td>
+        <td class="py-2 px-3 text-slate-400 font-sans truncate max-w-xs">${escapeHtml(cl.comment || '')}</td>
+        <td class="py-2 px-3 text-right space-x-1 whitespace-nowrap font-sans" onclick="event.stopPropagation()">
+          <button onclick="toggleCommunityListState('${cl.id}', ${!cl.disabled})" class="px-2 py-0.5 rounded text-[10px] ${cl.disabled ? 'bg-emerald-950 hover:bg-emerald-900 text-emerald-400 border border-emerald-800/50' : 'bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700'}">
+            ${cl.disabled ? 'Enable' : 'Disable'}
+          </button>
+          <button onclick="openEditCommunityListModal('${cl.id}')" class="px-2 py-0.5 rounded text-[10px] bg-slate-800 hover:bg-slate-700 text-emerald-300 border border-slate-700">
+            Edit
+          </button>
+          <button onclick="deleteCommunityList('${cl.id}')" class="px-2 py-0.5 rounded text-[10px] bg-rose-950/60 hover:bg-rose-900 text-rose-300 border border-rose-900/50">
+            ✕
+          </button>
+        </td>
+      </tr>
+    `;
+  }).join('');
+}
+
+// -------------------------------------------------------------
+// TAB 4: AS LISTS (/routing filter num-list)
+// -------------------------------------------------------------
+
+function renderRoutingFilterASLists() {
+  const tbody = document.getElementById('routing-filter-as-lists-tbody');
+  if (!tbody) return;
+  const empty = document.getElementById('rf-as-lists-empty');
+  const statusCount = document.getElementById('rf-status-count');
+
+  let list = (state.routingASLists || []).slice();
+
+  const q = state.rfSearchQuery;
+  if (q) {
+    list = list.filter(al =>
+      (al.name && al.name.toLowerCase().includes(q)) ||
+      (al.range && al.range.toLowerCase().includes(q)) ||
+      (al.comment && al.comment.toLowerCase().includes(q))
+    );
+  }
+
+  if (statusCount && state.activeRoutingFilterTab === 'as-lists') {
+    statusCount.innerText = `${list.length} item${list.length === 1 ? '' : 's'}`;
+  }
+
+  if (list.length === 0) {
+    tbody.innerHTML = '';
+    if (empty) empty.classList.remove('hidden');
+    return;
+  }
+  if (empty) empty.classList.add('hidden');
+
+  tbody.innerHTML = list.map(al => {
+    const isSelected = state.selectedFilterRow?.['as-lists'] === al.id;
+    const flagA = !al.disabled ? '<span class="text-cyan-400 font-bold">A</span>' : '';
+    const flagX = al.disabled ? '<span class="text-rose-400 font-bold">X</span>' : '';
+
+    return `
+      <tr onclick="selectFilterRow('as-lists', '${al.id}')" class="cursor-pointer transition select-none ${isSelected ? 'bg-emerald-950/40 ring-1 ring-emerald-500/40' : 'hover:bg-slate-800/40'}">
+        <td class="py-2 px-2 text-center tracking-wider">${flagA}${flagX}</td>
+        <td class="py-2 px-3 font-mono font-bold text-white">${escapeHtml(al.name)}</td>
+        <td class="py-2 px-3 font-mono text-purple-300">${escapeHtml(al.range)}</td>
+        <td class="py-2 px-3 text-slate-400 font-sans truncate max-w-xs">${escapeHtml(al.comment || '')}</td>
+        <td class="py-2 px-3 text-right space-x-1 whitespace-nowrap font-sans" onclick="event.stopPropagation()">
+          <button onclick="toggleASListState('${al.id}', ${!al.disabled})" class="px-2 py-0.5 rounded text-[10px] ${al.disabled ? 'bg-emerald-950 hover:bg-emerald-900 text-emerald-400 border border-emerald-800/50' : 'bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700'}">
+            ${al.disabled ? 'Enable' : 'Disable'}
+          </button>
+          <button onclick="openEditASListModal('${al.id}')" class="px-2 py-0.5 rounded text-[10px] bg-slate-800 hover:bg-slate-700 text-emerald-300 border border-slate-700">
+            Edit
+          </button>
+          <button onclick="deleteASList('${al.id}')" class="px-2 py-0.5 rounded text-[10px] bg-rose-950/60 hover:bg-rose-900 text-rose-300 border border-rose-900/50">
+            ✕
+          </button>
+        </td>
+      </tr>
+    `;
+  }).join('');
+}
+
+// -------------------------------------------------------------
+// SNIPPETS & WIZARD HELPERS
+// -------------------------------------------------------------
+
+function insertRuleSnippet(type) {
+  const textarea = document.getElementById('modal-rf-rule');
+  if (!textarea) return;
+  let snippet = '';
+  switch (type) {
+    case 'accept-default':
+      snippet = 'if (dst in 0.0.0.0/0) { accept; }';
+      break;
+    case 'drop-rfc1918':
+      snippet = 'if (dst in 10.0.0.0/8 || dst in 172.16.0.0/12 || dst in 192.168.0.0/16) { reject; }';
+      break;
+    case 'len-24':
+      snippet = 'if (dst-len > 24) { reject; }';
+      break;
+    case 'local-pref-150':
+      snippet = 'if (dst in 10.0.0.0/8) { set bgp-local-pref 150; accept; }';
+      break;
+    case 'as-prepend-2':
+      snippet = 'set bgp-path-prepend 2; accept;';
+      break;
+    case 'set-comm':
+      snippet = 'set bgp-communities.add 65000:100; accept;';
+      break;
+    case 'match-comm':
+      snippet = 'if (bgp-communities includes 65000:100) { accept; }';
+      break;
+    case 'reject-all':
+      snippet = 'reject;';
+      break;
+  }
+  if (!snippet) return;
+  if (!textarea.value.trim()) {
+    textarea.value = snippet;
+  } else {
+    textarea.value = textarea.value.trim() + '\n' + snippet;
+  }
+}
+
+function generateScriptFromWizard() {
+  const pfx = (document.getElementById('modal-rf-prefix')?.value || '').trim();
+  const pfxLen = (document.getElementById('modal-rf-prefix-len')?.value || '').trim();
+  const proto = document.getElementById('modal-rf-protocol')?.value || 'any';
+  const asp = (document.getElementById('modal-rf-as-path')?.value || '').trim();
+  const comm = (document.getElementById('modal-rf-community')?.value || '').trim();
+  const act = document.getElementById('modal-rf-action')?.value || 'accept';
+  const lp = parseInt(document.getElementById('modal-rf-set-local-pref')?.value, 10) || 0;
+  const prep = parseInt(document.getElementById('modal-rf-set-as-prepend')?.value, 10) || 0;
+
+  const conds = [];
+  if (pfx) {
+    if (pfxLen) {
+      if (pfxLen.includes('-')) {
+        const parts = pfxLen.split('-');
+        conds.push(`dst in ${pfx}{${parts[0].trim()},${parts[1].trim()}}`);
+      } else {
+        conds.push(`dst in ${pfx} && dst-len >= ${pfxLen}`);
+      }
+    } else {
+      conds.push(`dst in ${pfx}`);
+    }
+  }
+  if (proto && proto !== 'any') {
+    conds.push(`protocol ${proto}`);
+  }
+  if (asp) {
+    conds.push(`bgp-as-path "${asp}"`);
+  }
+  if (comm) {
+    conds.push(`bgp-communities includes ${comm}`);
+  }
+
+  const actions = [];
+  if (lp > 0) actions.push(`set bgp-local-pref ${lp};`);
+  if (prep > 0) actions.push(`set bgp-path-prepend ${prep};`);
+  actions.push(`${act};`);
+
+  let code = '';
+  if (conds.length > 0) {
+    code = `if (${conds.join(' && ')}) {\n    ${actions.join(' ')}\n}`;
+  } else {
+    code = actions.join(' ');
+  }
+
+  const textarea = document.getElementById('modal-rf-rule');
+  if (textarea) textarea.value = code;
 }
 
+// -------------------------------------------------------------
+// MODAL CRUD: RULES (/routing filter rule)
+// -------------------------------------------------------------
+
 function openAddRoutingFilterModal(defaultChain = '') {
   const form = document.getElementById('routing-filter-form');
   if (form) form.reset();
@@ -9769,10 +10175,8 @@ function openAddRoutingFilterModal(defaultChain = '') {
   if (chainEl) chainEl.value = defaultChain || (state.filterChainSelected && state.filterChainSelected !== 'all' ? state.filterChainSelected : 'bgp-in');
   const orderEl = document.getElementById('modal-rf-order');
   if (orderEl) orderEl.value = ((state.routingFilters || []).length + 1) * 10;
-  const actEl = document.getElementById('modal-rf-action');
-  if (actEl) actEl.value = 'accept';
-  const protoEl = document.getElementById('modal-rf-protocol');
-  if (protoEl) protoEl.value = 'any';
+  const ruleEl = document.getElementById('modal-rf-rule');
+  if (ruleEl) ruleEl.value = 'if (dst in 10.0.0.0/8) { set bgp-local-pref 150; accept; }';
   const disEl = document.getElementById('modal-rf-disabled');
   if (disEl) disEl.checked = false;
   const modal = document.getElementById('modal-routing-filter');
@@ -9790,28 +10194,18 @@ function openEditRoutingFilterModal(id) {
   if (chainEl) chainEl.value = r.chain || '';
   const orderEl = document.getElementById('modal-rf-order');
   if (orderEl) orderEl.value = r.rule_order || 10;
-  const actEl = document.getElementById('modal-rf-action');
-  if (actEl) actEl.value = r.action || 'accept';
-  const pfxEl = document.getElementById('modal-rf-prefix');
-  if (pfxEl) pfxEl.value = r.prefix || '';
-  const pfxLenEl = document.getElementById('modal-rf-prefix-len');
-  if (pfxLenEl) pfxLenEl.value = r.prefix_len || '';
-  const protoEl = document.getElementById('modal-rf-protocol');
-  if (protoEl) protoEl.value = r.match_protocol || 'any';
-  const aspEl = document.getElementById('modal-rf-as-path');
-  if (aspEl) aspEl.value = r.match_as_path || '';
-  const commEl = document.getElementById('modal-rf-community');
-  if (commEl) commEl.value = r.match_community || '';
-  const lpEl = document.getElementById('modal-rf-set-local-pref');
-  if (lpEl) lpEl.value = r.set_local_pref || '';
-  const metEl = document.getElementById('modal-rf-set-metric');
-  if (metEl) metEl.value = r.set_metric || '';
-  const prepEl = document.getElementById('modal-rf-set-as-prepend');
-  if (prepEl) prepEl.value = r.set_as_prepend || '';
-  const setCommEl = document.getElementById('modal-rf-set-community');
-  if (setCommEl) setCommEl.value = r.set_community || '';
-  const nhEl = document.getElementById('modal-rf-set-next-hop');
-  if (nhEl) nhEl.value = r.set_next_hop || '';
+  const ruleEl = document.getElementById('modal-rf-rule');
+  if (ruleEl) {
+    if (r.rule) {
+      ruleEl.value = r.rule;
+    } else {
+      let conds = [];
+      if (r.prefix) conds.push(`dst in ${r.prefix}`);
+      if (r.match_protocol && r.match_protocol !== 'any') conds.push(`protocol ${r.match_protocol}`);
+      let act = r.action || 'accept';
+      ruleEl.value = `if (${conds.length ? conds.join(' && ') : 'true'}) { ${act}; }`;
+    }
+  }
   const comEl = document.getElementById('modal-rf-comment');
   if (comEl) comEl.value = r.comment || '';
   const disEl = document.getElementById('modal-rf-disabled');
@@ -9826,22 +10220,12 @@ function closeRoutingFilterModal() {
 }
 
 async function submitRoutingFilter(e) {
-  e.preventDefault();
+  if (e) e.preventDefault();
   const payload = {
     id: document.getElementById('modal-rf-id').value,
     chain: document.getElementById('modal-rf-chain').value.trim(),
     rule_order: parseInt(document.getElementById('modal-rf-order').value, 10) || 10,
-    action: document.getElementById('modal-rf-action').value,
-    prefix: document.getElementById('modal-rf-prefix').value.trim(),
-    prefix_len: document.getElementById('modal-rf-prefix-len').value.trim(),
-    match_protocol: document.getElementById('modal-rf-protocol').value,
-    match_as_path: document.getElementById('modal-rf-as-path').value.trim(),
-    match_community: document.getElementById('modal-rf-community').value.trim(),
-    set_local_pref: parseInt(document.getElementById('modal-rf-set-local-pref').value, 10) || 0,
-    set_metric: parseInt(document.getElementById('modal-rf-set-metric').value, 10) || 0,
-    set_as_prepend: parseInt(document.getElementById('modal-rf-set-as-prepend').value, 10) || 0,
-    set_community: document.getElementById('modal-rf-set-community').value.trim(),
-    set_next_hop: document.getElementById('modal-rf-set-next-hop').value.trim(),
+    rule: document.getElementById('modal-rf-rule').value.trim(),
     comment: document.getElementById('modal-rf-comment').value.trim(),
     disabled: document.getElementById('modal-rf-disabled').checked
   };
@@ -9865,6 +10249,66 @@ async function submitRoutingFilter(e) {
   }
 }
 
+async function applyRoutingFilterForm() {
+  const payload = {
+    id: document.getElementById('modal-rf-id').value,
+    chain: document.getElementById('modal-rf-chain').value.trim(),
+    rule_order: parseInt(document.getElementById('modal-rf-order').value, 10) || 10,
+    rule: document.getElementById('modal-rf-rule').value.trim(),
+    comment: document.getElementById('modal-rf-comment').value.trim(),
+    disabled: document.getElementById('modal-rf-disabled').checked
+  };
+
+  try {
+    const res = await fetch(`${API_BASE}/routing/filters`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(payload)
+    });
+    if (res.ok) {
+      const data = await res.json();
+      if (data.id) document.getElementById('modal-rf-id').value = data.id;
+      showToast('Applied', 'Routing filter configuration applied', 'success');
+      loadDynamicRouting();
+    } else {
+      const err = await res.json();
+      showToast('Error', err.error || 'Failed to apply filter rule', 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function moveRoutingFilterOrder(id, delta) {
+  const list = state.routingFilters || [];
+  const idx = list.findIndex(r => r.id === id);
+  if (idx < 0) return;
+  const targetIdx = idx + delta;
+  if (targetIdx < 0 || targetIdx >= list.length) return;
+
+  const currentOrder = list[idx].rule_order || 10;
+  const targetOrder = list[targetIdx].rule_order || 20;
+
+  list[idx].rule_order = targetOrder;
+  list[targetIdx].rule_order = currentOrder;
+
+  try {
+    await fetch(`${API_BASE}/routing/filters`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(list[idx])
+    });
+    await fetch(`${API_BASE}/routing/filters`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(list[targetIdx])
+    });
+    loadDynamicRouting();
+  } catch (e) {
+    console.error('Reorder error:', e);
+  }
+}
+
 async function deleteRoutingFilter(id) {
   if (!confirm('Are you sure you want to delete this routing filter rule?')) return;
   try {
@@ -9900,6 +10344,297 @@ async function toggleRoutingFilterState(id, disabled) {
   }
 }
 
+// -------------------------------------------------------------
+// MODAL CRUD: SELECT RULES (/routing filter select-rule)
+// -------------------------------------------------------------
+
+function openAddSelectRuleModal() {
+  document.getElementById('modal-rf-sr-id').value = '';
+  document.getElementById('modal-rf-sr-title').innerText = 'Add Routing Select Rule';
+  document.getElementById('modal-rf-sr-chain').value = state.filterChainSelected && state.filterChainSelected !== 'all' ? state.filterChainSelected : 'bgp-out';
+  document.getElementById('modal-rf-sr-order').value = ((state.routingSelectRules || []).length + 1) * 10;
+  document.getElementById('modal-rf-sr-rule').value = 'if (protocol bgp && dst in 10.0.0.0/8) { accept; }';
+  document.getElementById('modal-rf-sr-comment').value = '';
+  document.getElementById('modal-rf-sr-disabled').checked = false;
+  document.getElementById('modal-rf-select-rule').classList.remove('hidden');
+}
+
+function openEditSelectRuleModal(id) {
+  const r = (state.routingSelectRules || []).find(item => item.id === id);
+  if (!r) return;
+  document.getElementById('modal-rf-sr-id').value = r.id;
+  document.getElementById('modal-rf-sr-title').innerText = 'Edit Routing Select Rule';
+  document.getElementById('modal-rf-sr-chain').value = r.chain || '';
+  document.getElementById('modal-rf-sr-order').value = r.rule_order || 10;
+  document.getElementById('modal-rf-sr-rule').value = r.rule || '';
+  document.getElementById('modal-rf-sr-comment').value = r.comment || '';
+  document.getElementById('modal-rf-sr-disabled').checked = !!r.disabled;
+  document.getElementById('modal-rf-select-rule').classList.remove('hidden');
+}
+
+function closeSelectRuleModal() {
+  document.getElementById('modal-rf-select-rule').classList.add('hidden');
+}
+
+async function submitSelectRule(e) {
+  e.preventDefault();
+  const payload = {
+    id: document.getElementById('modal-rf-sr-id').value,
+    chain: document.getElementById('modal-rf-sr-chain').value.trim(),
+    rule_order: parseInt(document.getElementById('modal-rf-sr-order').value, 10) || 10,
+    rule: document.getElementById('modal-rf-sr-rule').value.trim(),
+    comment: document.getElementById('modal-rf-sr-comment').value.trim(),
+    disabled: document.getElementById('modal-rf-sr-disabled').checked
+  };
+
+  try {
+    const res = await fetch(`${API_BASE}/routing/filters/select-rules`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(payload)
+    });
+    if (res.ok) {
+      closeSelectRuleModal();
+      showToast('Success', 'Select rule saved', 'success');
+      loadDynamicRouting();
+    } else {
+      const err = await res.json();
+      showToast('Error', err.error || 'Failed to save select rule', 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function deleteSelectRule(id) {
+  if (!confirm('Are you sure you want to delete this select rule?')) return;
+  try {
+    const res = await fetch(`${API_BASE}/routing/filters/select-rules/${encodeURIComponent(id)}`, { method: 'DELETE' });
+    if (res.ok) {
+      showToast('Deleted', 'Select rule removed', 'info');
+      loadDynamicRouting();
+    } else {
+      const err = await res.json();
+      showToast('Error', err.error || 'Failed to delete select rule', 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function toggleSelectRuleState(id, disabled) {
+  try {
+    const res = await fetch(`${API_BASE}/routing/filters/select-rules/${encodeURIComponent(id)}/state`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ disabled })
+    });
+    if (res.ok) {
+      showToast('Status Updated', `Select rule ${disabled ? 'disabled' : 'enabled'}`, 'info');
+      loadDynamicRouting();
+    } else {
+      const err = await res.json();
+      showToast('Error', err.error || 'Failed to update select rule state', 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+// -------------------------------------------------------------
+// MODAL CRUD: COMMUNITY LISTS (/routing filter community-list)
+// -------------------------------------------------------------
+
+function openAddCommunityListModal() {
+  document.getElementById('modal-rf-cl-id').value = '';
+  document.getElementById('modal-rf-cl-title').innerText = 'Add BGP Community List';
+  document.getElementById('modal-rf-cl-name').value = '';
+  document.getElementById('modal-rf-cl-communities').value = '';
+  document.getElementById('modal-rf-cl-regexp').value = '';
+  document.getElementById('modal-rf-cl-comment').value = '';
+  document.getElementById('modal-rf-cl-disabled').checked = false;
+  document.getElementById('modal-rf-community-list').classList.remove('hidden');
+}
+
+function openEditCommunityListModal(id) {
+  const cl = (state.routingCommunityLists || []).find(item => item.id === id);
+  if (!cl) return;
+  document.getElementById('modal-rf-cl-id').value = cl.id;
+  document.getElementById('modal-rf-cl-title').innerText = 'Edit BGP Community List';
+  document.getElementById('modal-rf-cl-name').value = cl.name || '';
+  document.getElementById('modal-rf-cl-communities').value = (cl.communities || []).join(', ');
+  document.getElementById('modal-rf-cl-regexp').value = cl.regexp || '';
+  document.getElementById('modal-rf-cl-comment').value = cl.comment || '';
+  document.getElementById('modal-rf-cl-disabled').checked = !!cl.disabled;
+  document.getElementById('modal-rf-community-list').classList.remove('hidden');
+}
+
+function closeCommunityListModal() {
+  document.getElementById('modal-rf-community-list').classList.add('hidden');
+}
+
+async function submitCommunityList(e) {
+  e.preventDefault();
+  const rawComms = document.getElementById('modal-rf-cl-communities').value;
+  const comms = rawComms.split(',').map(c => c.trim()).filter(Boolean);
+
+  const payload = {
+    id: document.getElementById('modal-rf-cl-id').value,
+    name: document.getElementById('modal-rf-cl-name').value.trim(),
+    communities: comms,
+    regexp: document.getElementById('modal-rf-cl-regexp').value.trim(),
+    comment: document.getElementById('modal-rf-cl-comment').value.trim(),
+    disabled: document.getElementById('modal-rf-cl-disabled').checked
+  };
+
+  try {
+    const res = await fetch(`${API_BASE}/routing/filters/community-lists`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(payload)
+    });
+    if (res.ok) {
+      closeCommunityListModal();
+      showToast('Success', 'Community list saved', 'success');
+      loadDynamicRouting();
+    } else {
+      const err = await res.json();
+      showToast('Error', err.error || 'Failed to save community list', 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function deleteCommunityList(id) {
+  if (!confirm('Are you sure you want to delete this community list?')) return;
+  try {
+    const res = await fetch(`${API_BASE}/routing/filters/community-lists/${encodeURIComponent(id)}`, { method: 'DELETE' });
+    if (res.ok) {
+      showToast('Deleted', 'Community list removed', 'info');
+      loadDynamicRouting();
+    } else {
+      const err = await res.json();
+      showToast('Error', err.error || 'Failed to delete community list', 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function toggleCommunityListState(id, disabled) {
+  try {
+    const res = await fetch(`${API_BASE}/routing/filters/community-lists/${encodeURIComponent(id)}/state`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ disabled })
+    });
+    if (res.ok) {
+      showToast('Status Updated', `Community list ${disabled ? 'disabled' : 'enabled'}`, 'info');
+      loadDynamicRouting();
+    } else {
+      const err = await res.json();
+      showToast('Error', err.error || 'Failed to update community list state', 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+// -------------------------------------------------------------
+// MODAL CRUD: AS LISTS (/routing filter num-list)
+// -------------------------------------------------------------
+
+function openAddASListModal() {
+  document.getElementById('modal-rf-al-id').value = '';
+  document.getElementById('modal-rf-al-title').innerText = 'Add AS Number List';
+  document.getElementById('modal-rf-al-name').value = '';
+  document.getElementById('modal-rf-al-range').value = '';
+  document.getElementById('modal-rf-al-comment').value = '';
+  document.getElementById('modal-rf-al-disabled').checked = false;
+  document.getElementById('modal-rf-as-list').classList.remove('hidden');
+}
+
+function openEditASListModal(id) {
+  const al = (state.routingASLists || []).find(item => item.id === id);
+  if (!al) return;
+  document.getElementById('modal-rf-al-id').value = al.id;
+  document.getElementById('modal-rf-al-title').innerText = 'Edit AS Number List';
+  document.getElementById('modal-rf-al-name').value = al.name || '';
+  document.getElementById('modal-rf-al-range').value = al.range || '';
+  document.getElementById('modal-rf-al-comment').value = al.comment || '';
+  document.getElementById('modal-rf-al-disabled').checked = !!al.disabled;
+  document.getElementById('modal-rf-as-list').classList.remove('hidden');
+}
+
+function closeASListModal() {
+  document.getElementById('modal-rf-as-list').classList.add('hidden');
+}
+
+async function submitASList(e) {
+  e.preventDefault();
+  const payload = {
+    id: document.getElementById('modal-rf-al-id').value,
+    name: document.getElementById('modal-rf-al-name').value.trim(),
+    range: document.getElementById('modal-rf-al-range').value.trim(),
+    comment: document.getElementById('modal-rf-al-comment').value.trim(),
+    disabled: document.getElementById('modal-rf-al-disabled').checked
+  };
+
+  try {
+    const res = await fetch(`${API_BASE}/routing/filters/as-lists`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify(payload)
+    });
+    if (res.ok) {
+      closeASListModal();
+      showToast('Success', 'AS number list saved', 'success');
+      loadDynamicRouting();
+    } else {
+      const err = await res.json();
+      showToast('Error', err.error || 'Failed to save AS list', 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function deleteASList(id) {
+  if (!confirm('Are you sure you want to delete this AS list?')) return;
+  try {
+    const res = await fetch(`${API_BASE}/routing/filters/as-lists/${encodeURIComponent(id)}`, { method: 'DELETE' });
+    if (res.ok) {
+      showToast('Deleted', 'AS list removed', 'info');
+      loadDynamicRouting();
+    } else {
+      const err = await res.json();
+      showToast('Error', err.error || 'Failed to delete AS list', 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function toggleASListState(id, disabled) {
+  try {
+    const res = await fetch(`${API_BASE}/routing/filters/as-lists/${encodeURIComponent(id)}/state`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ disabled })
+    });
+    if (res.ok) {
+      showToast('Status Updated', `AS list ${disabled ? 'disabled' : 'enabled'}`, 'info');
+      loadDynamicRouting();
+    } else {
+      const err = await res.json();
+      showToast('Error', err.error || 'Failed to update AS list state', 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
 // ==========================================
 // TROUBLESHOOT & DIAGNOSTIC TOOLS (/tool)
 // ==========================================

+ 404 - 130
internal/web/static/index.html

@@ -2592,7 +2592,7 @@
         </section>
 
         <!-- ================= SECTION: ROUTING FILTERS (/routing filter) ================= -->
-        <section id="section-routing-filters" class="hidden space-y-6">
+        <section id="section-routing-filters" class="hidden space-y-4">
           <!-- Top Routing Switcher Bar -->
           <div class="flex items-center gap-1.5 p-1 bg-slate-900/90 border border-slate-800 rounded-xl w-fit">
             <button onclick="navigate('routes')" class="px-3.5 py-1.5 text-xs font-medium rounded-lg text-slate-400 hover:text-white hover:bg-slate-800/60 transition flex items-center gap-2">
@@ -2621,65 +2621,166 @@
             </button>
           </div>
 
-          <!-- Top Banner -->
-          <div class="bg-gradient-to-r from-emerald-950/40 via-slate-900 to-slate-900 border border-emerald-900/30 rounded-xl p-5 flex flex-wrap items-center justify-between gap-4">
-            <div>
-              <div class="flex items-center gap-2 mb-1">
-                <span class="px-2 py-0.5 rounded text-[10px] font-mono bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 font-semibold">/routing filter</span>
-                <h2 class="text-base font-bold text-white">Routing Filters & Policy Maps</h2>
+          <!-- RouterOS v7 Style Window Container -->
+          <div class="bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-2xl">
+            <!-- Window Titlebar with RouterOS v7 Tabs -->
+            <div class="bg-slate-950 border-b border-slate-800 px-3 py-1.5 flex flex-wrap items-center justify-between gap-2">
+              <div class="flex items-center gap-3">
+                <div class="flex items-center gap-2 text-emerald-400 font-bold text-xs">
+                  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
+                  </svg>
+                  <span>Routing Filters</span>
+                </div>
+
+                <!-- Navigation Tabs identical to RouterOS v7 -->
+                <div class="flex items-center gap-1 border-l border-slate-800 pl-3">
+                  <button id="tab-rf-rules" onclick="switchRoutingFilterTab('rules')" class="px-3 py-1 text-xs font-semibold rounded bg-emerald-600/20 text-emerald-300 border border-emerald-500/30 transition">
+                    Rules
+                  </button>
+                  <button id="tab-rf-select-rules" onclick="switchRoutingFilterTab('select-rules')" class="px-3 py-1 text-xs font-semibold rounded text-slate-400 hover:text-white hover:bg-slate-800 transition">
+                    Select Rules
+                  </button>
+                  <button id="tab-rf-community-lists" onclick="switchRoutingFilterTab('community-lists')" class="px-3 py-1 text-xs font-semibold rounded text-slate-400 hover:text-white hover:bg-slate-800 transition">
+                    Community Lists
+                  </button>
+                  <button id="tab-rf-as-lists" onclick="switchRoutingFilterTab('as-lists')" class="px-3 py-1 text-xs font-semibold rounded text-slate-400 hover:text-white hover:bg-slate-800 transition">
+                    AS Lists
+                  </button>
+                </div>
+              </div>
+
+              <div class="flex items-center gap-2 text-slate-400 text-xs">
+                <span class="px-2 py-0.5 rounded text-[10px] font-mono bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 font-semibold">BIRD 2 Engine</span>
+                <button onclick="loadDynamicRouting(true)" class="p-1 hover:text-white rounded" title="Reload routing">↻</button>
               </div>
-              <p class="text-xs text-slate-400">Policy routing filter chains (in-filter / out-filter), prefix-list evaluation, BGP community matching, and route metric manipulation.</p>
             </div>
 
-            <div class="flex items-center gap-2.5">
-              <div class="flex items-center gap-2">
-                <label class="text-[11px] text-slate-400 font-medium">Chain:</label>
-                <select id="routing-filter-chain-select" onchange="filterRoutingFiltersByChain(this.value)" class="bg-slate-800 border border-slate-700 rounded-lg px-2.5 py-1.5 text-xs text-white font-mono">
-                  <option value="all">All Chains</option>
-                </select>
+            <!-- MikroTik RouterOS v7 Toolbar -->
+            <div class="bg-slate-900/90 border-b border-slate-800 px-3 py-2 flex flex-wrap items-center justify-between gap-2 text-xs">
+              <div class="flex items-center gap-1.5">
+                <button onclick="openRoutingFilterNewModal()" class="px-2.5 py-1 rounded font-semibold bg-emerald-600 hover:bg-emerald-500 text-white shadow-sm flex items-center gap-1">
+                  <span>+ New</span>
+                </button>
+                <button id="btn-rf-enable-selected" onclick="toggleSelectedRoutingFilter(false)" class="px-2.5 py-1 rounded font-medium bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 transition">
+                  Enable
+                </button>
+                <button id="btn-rf-disable-selected" onclick="toggleSelectedRoutingFilter(true)" class="px-2.5 py-1 rounded font-medium bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 transition">
+                  Disable
+                </button>
+                <button id="btn-rf-remove-selected" onclick="removeSelectedRoutingFilter()" class="px-2.5 py-1 rounded font-medium bg-slate-800 hover:bg-rose-950 text-rose-400 border border-slate-700 transition">
+                  Remove
+                </button>
+              </div>
+
+              <div class="flex items-center gap-2.5">
+                <!-- Filter by Chain (Visible in Rules and Select Rules tabs) -->
+                <div id="rf-chain-filter-container" class="flex items-center gap-1.5">
+                  <label class="text-[11px] text-slate-400 font-medium">Chain:</label>
+                  <select id="routing-filter-chain-select" onchange="filterRoutingFiltersByChain(this.value)" class="bg-slate-800 border border-slate-700 rounded px-2 py-1 text-xs text-white font-mono">
+                    <option value="all">All Chains</option>
+                  </select>
+                </div>
+
+                <!-- Find / Search Input -->
+                <div class="relative">
+                  <input type="text" id="rf-search-input" oninput="searchRoutingFilters(this.value)" placeholder="Find..." class="bg-slate-800 border border-slate-700 rounded px-2.5 py-1 text-xs text-white pl-6 w-36 focus:w-48 transition-all">
+                  <svg class="w-3 h-3 text-slate-400 absolute left-2 top-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                    <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
+                  </svg>
+                </div>
               </div>
-              <button onclick="openAddRoutingFilterModal()" class="px-3.5 py-1.5 rounded-lg text-xs font-semibold bg-emerald-600 hover:bg-emerald-500 text-white shadow-lg shadow-emerald-500/20 transition flex items-center gap-1.5">
-                <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
-                  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
-                </svg>
-                <span>+ Add Filter Rule</span>
-              </button>
-              <button onclick="loadDynamicRouting(true)" class="px-3 py-1.5 rounded-lg text-xs font-medium bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 transition" title="Refresh routing telemetry">
-                ↻
-              </button>
             </div>
-          </div>
 
-          <!-- Filter Rules Table -->
-          <div class="bg-slate-900/90 border border-slate-800 rounded-xl overflow-hidden">
-            <div class="p-4 border-b border-slate-800 flex items-center justify-between">
-              <div class="flex items-center gap-2">
-                <h3 class="text-sm font-bold text-white">Filter Rules</h3>
-                <span id="routing-filters-count-badge" class="px-2 py-0.5 rounded text-[10px] font-mono bg-emerald-500/10 text-emerald-400 border border-emerald-500/20 font-semibold">0 Rules</span>
+            <!-- TAB 1: RULES (/routing filter rule) -->
+            <div id="tab-content-rf-rules" class="overflow-x-auto">
+              <table class="w-full text-left text-xs font-mono">
+                <thead class="text-slate-400 bg-slate-950/70 border-b border-slate-800 uppercase tracking-wider text-[10px] font-sans select-none">
+                  <tr>
+                    <th class="py-2 px-2 text-center w-10 cursor-pointer hover:text-white" onclick="sortFilterTable('rules', 'flags')">Flags</th>
+                    <th class="py-2 px-2 w-12 cursor-pointer hover:text-white" onclick="sortFilterTable('rules', 'rule_order')"># <span id="sort-icon-rules-rule_order">▲</span></th>
+                    <th class="py-2 px-3 cursor-pointer hover:text-white" onclick="sortFilterTable('rules', 'chain')">Chain <span id="sort-icon-rules-chain"></span></th>
+                    <th class="py-2 px-3 cursor-pointer hover:text-white" onclick="sortFilterTable('rules', 'rule')">Rule (ROS v7 Script) <span id="sort-icon-rules-rule"></span></th>
+                    <th class="py-2 px-3 cursor-pointer hover:text-white" onclick="sortFilterTable('rules', 'comment')">Comment <span id="sort-icon-rules-comment"></span></th>
+                    <th class="py-2 px-3 text-right font-sans">Actions</th>
+                  </tr>
+                </thead>
+                <tbody id="routing-filter-rules-tbody" class="divide-y divide-slate-800/60"></tbody>
+              </table>
+              <div id="rf-rules-empty" class="hidden p-8 text-center text-slate-500 text-xs font-sans">
+                No filter rules configured. Click "+ New" to add a RouterOS v7 filter rule.
               </div>
-              <span class="text-xs text-slate-500 font-sans">Evaluated sequentially by order</span>
             </div>
 
-            <div class="overflow-x-auto">
+            <!-- TAB 2: SELECT RULES (/routing filter select-rule) -->
+            <div id="tab-content-rf-select-rules" class="hidden overflow-x-auto">
               <table class="w-full text-left text-xs font-mono">
-                <thead class="text-slate-400 bg-slate-950/60 border-b border-slate-800 uppercase tracking-wider text-[10px] font-sans">
+                <thead class="text-slate-400 bg-slate-950/70 border-b border-slate-800 uppercase tracking-wider text-[10px] font-sans select-none">
                   <tr>
-                    <th class="py-2.5 px-3 text-center w-12">Flags</th>
-                    <th class="py-2.5 px-3 w-16">#</th>
-                    <th class="py-2.5 px-3">Chain</th>
-                    <th class="py-2.5 px-3">Action</th>
-                    <th class="py-2.5 px-3">Match Prefix / Length</th>
-                    <th class="py-2.5 px-3">Match AS-Path / Community</th>
-                    <th class="py-2.5 px-3">Set Modifications</th>
-                    <th class="py-2.5 px-3">Comment</th>
-                    <th class="py-2.5 px-3 text-right font-sans">Actions</th>
+                    <th class="py-2 px-2 text-center w-10 cursor-pointer hover:text-white" onclick="sortFilterTable('select-rules', 'flags')">Flags</th>
+                    <th class="py-2 px-2 w-12 cursor-pointer hover:text-white" onclick="sortFilterTable('select-rules', 'rule_order')"># <span id="sort-icon-select-rules-rule_order">▲</span></th>
+                    <th class="py-2 px-3 cursor-pointer hover:text-white" onclick="sortFilterTable('select-rules', 'chain')">Chain <span id="sort-icon-select-rules-chain"></span></th>
+                    <th class="py-2 px-3 cursor-pointer hover:text-white" onclick="sortFilterTable('select-rules', 'rule')">Rule (ROS v7 Script) <span id="sort-icon-select-rules-rule"></span></th>
+                    <th class="py-2 px-3 cursor-pointer hover:text-white" onclick="sortFilterTable('select-rules', 'comment')">Comment <span id="sort-icon-select-rules-comment"></span></th>
+                    <th class="py-2 px-3 text-right font-sans">Actions</th>
+                  </tr>
+                </thead>
+                <tbody id="routing-filter-select-rules-tbody" class="divide-y divide-slate-800/60"></tbody>
+              </table>
+              <div id="rf-select-rules-empty" class="hidden p-8 text-center text-slate-500 text-xs font-sans">
+                No select rules configured. Click "+ New" to add a route selection rule.
+              </div>
+            </div>
+
+            <!-- TAB 3: COMMUNITY LISTS (/routing filter community-list) -->
+            <div id="tab-content-rf-community-lists" class="hidden overflow-x-auto">
+              <table class="w-full text-left text-xs font-mono">
+                <thead class="text-slate-400 bg-slate-950/70 border-b border-slate-800 uppercase tracking-wider text-[10px] font-sans select-none">
+                  <tr>
+                    <th class="py-2 px-2 text-center w-10 cursor-pointer hover:text-white" onclick="sortFilterTable('community-lists', 'flags')">Flags</th>
+                    <th class="py-2 px-3 cursor-pointer hover:text-white" onclick="sortFilterTable('community-lists', 'name')">List Name <span id="sort-icon-community-lists-name">▲</span></th>
+                    <th class="py-2 px-3 cursor-pointer hover:text-white" onclick="sortFilterTable('community-lists', 'communities')">Communities <span id="sort-icon-community-lists-communities"></span></th>
+                    <th class="py-2 px-3 cursor-pointer hover:text-white" onclick="sortFilterTable('community-lists', 'regexp')">RegExp <span id="sort-icon-community-lists-regexp"></span></th>
+                    <th class="py-2 px-3 cursor-pointer hover:text-white" onclick="sortFilterTable('community-lists', 'comment')">Comment <span id="sort-icon-community-lists-comment"></span></th>
+                    <th class="py-2 px-3 text-right font-sans">Actions</th>
                   </tr>
                 </thead>
-                <tbody id="routing-filters-table-body" class="divide-y divide-slate-800/80"></tbody>
+                <tbody id="routing-filter-comm-lists-tbody" class="divide-y divide-slate-800/60"></tbody>
               </table>
+              <div id="rf-comm-lists-empty" class="hidden p-8 text-center text-slate-500 text-xs font-sans">
+                No BGP community lists configured. Click "+ New" to add a named community set.
+              </div>
+            </div>
+
+            <!-- TAB 4: AS LISTS (/routing filter num-list) -->
+            <div id="tab-content-rf-as-lists" class="hidden overflow-x-auto">
+              <table class="w-full text-left text-xs font-mono">
+                <thead class="text-slate-400 bg-slate-950/70 border-b border-slate-800 uppercase tracking-wider text-[10px] font-sans select-none">
+                  <tr>
+                    <th class="py-2 px-2 text-center w-10 cursor-pointer hover:text-white" onclick="sortFilterTable('as-lists', 'flags')">Flags</th>
+                    <th class="py-2 px-3 cursor-pointer hover:text-white" onclick="sortFilterTable('as-lists', 'name')">List Name <span id="sort-icon-as-lists-name">▲</span></th>
+                    <th class="py-2 px-3 cursor-pointer hover:text-white" onclick="sortFilterTable('as-lists', 'range')">Range / ASNs <span id="sort-icon-as-lists-range"></span></th>
+                    <th class="py-2 px-3 cursor-pointer hover:text-white" onclick="sortFilterTable('as-lists', 'comment')">Comment <span id="sort-icon-as-lists-comment"></span></th>
+                    <th class="py-2 px-3 text-right font-sans">Actions</th>
+                  </tr>
+                </thead>
+                <tbody id="routing-filter-as-lists-tbody" class="divide-y divide-slate-800/60"></tbody>
+              </table>
+              <div id="rf-as-lists-empty" class="hidden p-8 text-center text-slate-500 text-xs font-sans">
+                No AS number lists configured. Click "+ New" to define reusable AS ranges.
+              </div>
             </div>
-            <div id="routing-filters-empty" class="hidden p-10 text-center text-slate-500 text-xs font-sans">
-              No routing filter rules configured. Click "+ Add Filter Rule" to create a filter chain.
+
+            <!-- Live Status Footer -->
+            <div class="px-3 py-1.5 bg-slate-950/80 border-t border-slate-800 flex items-center justify-between text-[11px] text-slate-400">
+              <div class="flex items-center gap-2">
+                <span id="rf-status-count" class="font-bold text-white">0</span>
+                <span class="flex items-center gap-1 text-emerald-400">
+                  <span class="w-1.5 h-1.5 rounded-full bg-emerald-400 animate-pulse"></span>
+                  <span>Live</span>
+                </span>
+              </div>
+              <span class="text-[10px] text-slate-500 font-mono">MikroTik RouterOS v7 Filter Model & BIRD 2 Execution</span>
             </div>
           </div>
         </section>
@@ -8153,132 +8254,305 @@
     </div>
   </div>
 
-  <!-- ================= MODAL: ADD / EDIT ROUTING FILTER ================= -->
+  <!-- ================= MODAL: ADD / EDIT ROUTING FILTER RULE ================= -->
   <div id="modal-routing-filter" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
-    <div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-xl shadow-2xl overflow-hidden animate-in">
-      <div class="px-6 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/40">
-        <div>
-          <h3 id="modal-rf-title" class="text-sm font-bold text-white">Add Routing Filter Rule</h3>
-          <p class="text-[11px] text-slate-400">Match routing prefixes, AS-Path, communities, and set BGP attributes (/routing filter)</p>
+    <div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-2xl shadow-2xl overflow-hidden animate-in">
+      <div class="px-6 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/60">
+        <div class="flex items-center gap-2">
+          <svg class="w-4 h-4 text-emerald-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
+          </svg>
+          <div>
+            <h3 id="modal-rf-title" class="text-sm font-bold text-white">Routing Filter Rule</h3>
+            <p class="text-[11px] text-slate-400">RouterOS v7 procedural syntax: if (...) { ... } else { ... } (/routing filter rule)</p>
+          </div>
         </div>
         <button onclick="closeRoutingFilterModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
       </div>
+
       <form id="routing-filter-form" onsubmit="submitRoutingFilter(event)" class="p-6 space-y-4 text-xs max-h-[85vh] overflow-y-auto">
         <input type="hidden" id="modal-rf-id">
 
         <div class="grid grid-cols-3 gap-3">
-          <div class="col-span-1">
-            <label class="block font-semibold text-slate-300 mb-1.5">Chain Name *</label>
-            <input type="text" id="modal-rf-chain" required placeholder="bgp-in" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
-            <p class="text-[10px] text-slate-500 mt-0.5">e.g. bgp-in, bgp-out</p>
+          <div class="col-span-2">
+            <label class="block font-semibold text-slate-300 mb-1">Chain *</label>
+            <input type="text" id="modal-rf-chain" list="dl-routing-chains" required placeholder="e.g. bgp-in, bgp-out, awmn" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:border-emerald-500">
+            <datalist id="dl-routing-chains"></datalist>
           </div>
           <div>
-            <label class="block font-semibold text-slate-300 mb-1.5">Rule Order *</label>
-            <input type="number" id="modal-rf-order" value="10" min="1" max="65535" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
-            <p class="text-[10px] text-slate-500 mt-0.5">Sequence (10, 20...)</p>
+            <label class="block font-semibold text-slate-300 mb-1">Rule Order *</label>
+            <input type="number" id="modal-rf-order" value="10" min="1" max="65535" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:border-emerald-500">
           </div>
-          <div>
-            <label class="block font-semibold text-slate-300 mb-1.5">Action *</label>
-            <select id="modal-rf-action" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
-              <option value="accept">accept (Permit)</option>
-              <option value="discard">discard (Deny)</option>
-              <option value="reject">reject (Deny)</option>
-              <option value="passthrough">passthrough (Continue)</option>
-            </select>
-            <p class="text-[10px] text-slate-500 mt-0.5">Filter action</p>
+        </div>
+
+        <!-- RouterOS v7 Script Rule Editor -->
+        <div>
+          <div class="flex items-center justify-between mb-1.5">
+            <label class="font-semibold text-slate-300 flex items-center gap-2">
+              <span>Rule (RouterOS v7 Script Syntax) *</span>
+              <span class="text-[10px] text-emerald-400 font-mono bg-emerald-950/60 border border-emerald-800/40 px-1.5 py-0.5 rounded">v7 Script Engine</span>
+            </label>
+            <span class="text-[10px] text-slate-400">Terminates with accept; or reject;</span>
+          </div>
+
+          <!-- Quick Templates Bar -->
+          <div class="flex flex-wrap items-center gap-1.5 mb-2 p-2 bg-slate-950/60 border border-slate-800 rounded-lg">
+            <span class="text-[10px] text-slate-400 font-medium mr-1">Quick Templates:</span>
+            <button type="button" onclick="insertRuleSnippet('accept-default')" class="px-2 py-0.5 rounded text-[10px] bg-slate-800 hover:bg-slate-700 text-cyan-300 border border-slate-700">0.0.0.0/0</button>
+            <button type="button" onclick="insertRuleSnippet('drop-rfc1918')" class="px-2 py-0.5 rounded text-[10px] bg-slate-800 hover:bg-slate-700 text-rose-300 border border-slate-700">Drop RFC1918</button>
+            <button type="button" onclick="insertRuleSnippet('len-24')" class="px-2 py-0.5 rounded text-[10px] bg-slate-800 hover:bg-slate-700 text-amber-300 border border-slate-700">Len &le; /24</button>
+            <button type="button" onclick="insertRuleSnippet('local-pref-150')" class="px-2 py-0.5 rounded text-[10px] bg-slate-800 hover:bg-slate-700 text-emerald-300 border border-slate-700">Loc-Pref 150</button>
+            <button type="button" onclick="insertRuleSnippet('as-prepend-2')" class="px-2 py-0.5 rounded text-[10px] bg-slate-800 hover:bg-slate-700 text-purple-300 border border-slate-700">AS-Prepend 2x</button>
+            <button type="button" onclick="insertRuleSnippet('set-comm')" class="px-2 py-0.5 rounded text-[10px] bg-slate-800 hover:bg-slate-700 text-indigo-300 border border-slate-700">Add Comm</button>
+            <button type="button" onclick="insertRuleSnippet('match-comm')" class="px-2 py-0.5 rounded text-[10px] bg-slate-800 hover:bg-slate-700 text-indigo-300 border border-slate-700">Match Comm</button>
+            <button type="button" onclick="insertRuleSnippet('reject-all')" class="px-2 py-0.5 rounded text-[10px] bg-rose-950/60 hover:bg-rose-900 text-rose-300 border border-rose-900/50">Reject</button>
           </div>
+
+          <textarea id="modal-rf-rule" rows="4" required placeholder="if (dst in 10.0.0.0/8 &amp;&amp; dst-len &gt;= 24) { reject; }&#10;if (dst in 10.0.0.0/8) { set bgp-local-pref 150; accept; }" class="w-full bg-slate-950 border border-slate-700 rounded-lg p-3 text-emerald-400 font-mono text-xs focus:outline-none focus:border-emerald-500"></textarea>
         </div>
 
-        <!-- Match Criteria Box -->
-        <div class="p-3.5 rounded-xl bg-slate-800/40 border border-slate-700/50 space-y-3">
-          <div class="text-[11px] font-bold text-cyan-400 uppercase tracking-wider">Match Conditions</div>
-          
-          <div class="grid grid-cols-2 gap-3">
-            <div>
-              <label class="block font-medium text-slate-300 mb-1">Prefix (CIDR)</label>
-              <input type="text" id="modal-rf-prefix" placeholder="e.g. 10.0.0.0/8 or 0.0.0.0/0" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
-              <p class="text-[10px] text-slate-500 mt-0.5">Leave blank to match all prefixes</p>
-            </div>
-            <div>
-              <label class="block font-medium text-slate-300 mb-1">Prefix Length (Range)</label>
-              <input type="text" id="modal-rf-prefix-len" placeholder="e.g. 16-24 or ge 24 le 32" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
-              <p class="text-[10px] text-slate-500 mt-0.5">Optional subnet mask length filter</p>
+        <!-- Optional Visual Builder Collapsible Accordion -->
+        <details class="bg-slate-950/40 border border-slate-800 rounded-xl p-3 group">
+          <summary class="font-semibold text-slate-300 cursor-pointer flex items-center justify-between text-xs select-none">
+            <span class="flex items-center gap-1.5 text-cyan-400">
+              <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
+                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4" />
+              </svg>
+              <span>Visual Rule Wizard (Form Assistant)</span>
+            </span>
+            <span class="text-[10px] text-slate-500 group-open:rotate-180 transition-transform">▼</span>
+          </summary>
+
+          <div class="pt-3 space-y-3">
+            <div class="grid grid-cols-2 gap-3">
+              <div>
+                <label class="block font-medium text-slate-400 mb-1">Prefix (CIDR)</label>
+                <input type="text" id="modal-rf-prefix" placeholder="10.0.0.0/8" class="w-full bg-slate-800 border border-slate-700 rounded px-2.5 py-1.5 text-white font-mono">
+              </div>
+              <div>
+                <label class="block font-medium text-slate-400 mb-1">Prefix Length (Range)</label>
+                <input type="text" id="modal-rf-prefix-len" placeholder="24-32 or ge 24" class="w-full bg-slate-800 border border-slate-700 rounded px-2.5 py-1.5 text-white font-mono">
+              </div>
             </div>
-          </div>
 
-          <div class="grid grid-cols-3 gap-3">
-            <div>
-              <label class="block font-medium text-slate-300 mb-1">Match Protocol</label>
-              <select id="modal-rf-protocol" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
-                <option value="any">Any Protocol</option>
-                <option value="bgp">BGP</option>
-                <option value="ospf">OSPF</option>
-                <option value="connected">Connected</option>
-                <option value="static">Static</option>
-              </select>
+            <div class="grid grid-cols-3 gap-3">
+              <div>
+                <label class="block font-medium text-slate-400 mb-1">Protocol</label>
+                <select id="modal-rf-protocol" class="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-white font-mono">
+                  <option value="any">any</option>
+                  <option value="bgp">bgp</option>
+                  <option value="ospf">ospf</option>
+                  <option value="connected">connected</option>
+                  <option value="static">static</option>
+                </select>
+              </div>
+              <div>
+                <label class="block font-medium text-slate-400 mb-1">AS-Path</label>
+                <input type="text" id="modal-rf-as-path" placeholder="65000" class="w-full bg-slate-800 border border-slate-700 rounded px-2.5 py-1.5 text-white font-mono">
+              </div>
+              <div>
+                <label class="block font-medium text-slate-400 mb-1">Community</label>
+                <input type="text" id="modal-rf-community" placeholder="65000:100" class="w-full bg-slate-800 border border-slate-700 rounded px-2.5 py-1.5 text-white font-mono">
+              </div>
             </div>
-            <div>
-              <label class="block font-medium text-slate-300 mb-1">Match AS-Path (Regex)</label>
-              <input type="text" id="modal-rf-as-path" placeholder="e.g. ^65001_ or _65000$" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
+
+            <div class="grid grid-cols-3 gap-3">
+              <div>
+                <label class="block font-medium text-slate-400 mb-1">Action</label>
+                <select id="modal-rf-action" class="w-full bg-slate-800 border border-slate-700 rounded px-2 py-1.5 text-white font-mono">
+                  <option value="accept">accept</option>
+                  <option value="reject">reject</option>
+                </select>
+              </div>
+              <div>
+                <label class="block font-medium text-slate-400 mb-1">Set Local-Pref</label>
+                <input type="number" id="modal-rf-set-local-pref" placeholder="150" class="w-full bg-slate-800 border border-slate-700 rounded px-2.5 py-1.5 text-white font-mono">
+              </div>
+              <div>
+                <label class="block font-medium text-slate-400 mb-1">Set Prepend</label>
+                <input type="number" id="modal-rf-set-as-prepend" placeholder="2" class="w-full bg-slate-800 border border-slate-700 rounded px-2.5 py-1.5 text-white font-mono">
+              </div>
             </div>
-            <div>
-              <label class="block font-medium text-slate-300 mb-1">Match Community</label>
-              <input type="text" id="modal-rf-community" placeholder="e.g. 65001:100" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
+
+            <div class="flex justify-end">
+              <button type="button" onclick="generateScriptFromWizard()" class="px-3 py-1 bg-cyan-600 hover:bg-cyan-500 text-white rounded font-semibold text-xs flex items-center gap-1">
+                <span>Generate Script Expression &uarr;</span>
+              </button>
             </div>
           </div>
+        </details>
+
+        <div class="grid grid-cols-3 gap-3">
+          <div class="col-span-2">
+            <label class="block font-semibold text-slate-300 mb-1">Comment</label>
+            <input type="text" id="modal-rf-comment" placeholder="Filter customer routes" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white">
+          </div>
+          <div class="flex items-center pt-6">
+            <label class="flex items-center space-x-2 text-slate-300 cursor-pointer">
+              <input type="checkbox" id="modal-rf-disabled" class="rounded text-emerald-600 bg-slate-800 border-slate-700">
+              <span class="font-medium">Disabled (X)</span>
+            </label>
+          </div>
         </div>
 
-        <!-- Set Attributes Box (Modifications) -->
-        <div class="p-3.5 rounded-xl bg-slate-800/40 border border-slate-700/50 space-y-3">
-          <div class="text-[11px] font-bold text-purple-400 uppercase tracking-wider">Set Modifications (On Accept)</div>
-          
-          <div class="grid grid-cols-3 gap-3">
-            <div>
-              <label class="block font-medium text-slate-300 mb-1">Set BGP Local Pref</label>
-              <input type="number" id="modal-rf-set-local-pref" placeholder="e.g. 150" min="0" max="4294967295" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
-            </div>
-            <div>
-              <label class="block font-medium text-slate-300 mb-1">Set Metric / MED</label>
-              <input type="number" id="modal-rf-set-metric" placeholder="e.g. 20" min="0" max="4294967295" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
-            </div>
-            <div>
-              <label class="block font-medium text-slate-300 mb-1">Set AS Prepend (Count)</label>
-              <input type="number" id="modal-rf-set-as-prepend" placeholder="e.g. 2" min="1" max="10" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
-            </div>
+        <div class="pt-4 border-t border-slate-800 flex justify-end space-x-2">
+          <button type="button" onclick="closeRoutingFilterModal()" class="px-4 py-1.5 rounded-lg text-slate-400 hover:bg-slate-800">Cancel</button>
+          <button type="button" onclick="applyRoutingFilterForm()" class="px-4 py-1.5 rounded-lg font-semibold bg-slate-800 hover:bg-slate-700 text-emerald-300 border border-slate-700">Apply</button>
+          <button type="submit" class="px-5 py-1.5 rounded-lg font-bold bg-emerald-600 hover:bg-emerald-500 text-white shadow-lg shadow-emerald-500/20">OK</button>
+        </div>
+      </form>
+    </div>
+  </div>
+
+  <!-- ================= MODAL: SELECT RULE (/routing filter select-rule) ================= -->
+  <div id="modal-rf-select-rule" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
+    <div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in">
+      <div class="px-6 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/60">
+        <div>
+          <h3 id="modal-rf-sr-title" class="text-sm font-bold text-white">Routing Select Rule</h3>
+          <p class="text-[11px] text-slate-400">Controls route preference and candidate selection (/routing filter select-rule)</p>
+        </div>
+        <button onclick="closeSelectRuleModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
+      </div>
+
+      <form onsubmit="submitSelectRule(event)" class="p-6 space-y-4 text-xs">
+        <input type="hidden" id="modal-rf-sr-id">
+
+        <div class="grid grid-cols-3 gap-3">
+          <div class="col-span-2">
+            <label class="block font-semibold text-slate-300 mb-1">Chain *</label>
+            <input type="text" id="modal-rf-sr-chain" required placeholder="e.g. bgp-out" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1">Order *</label>
+            <input type="number" id="modal-rf-sr-order" value="10" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
           </div>
+        </div>
 
-          <div class="grid grid-cols-2 gap-3">
-            <div>
-              <label class="block font-medium text-slate-300 mb-1">Set Community</label>
-              <input type="text" id="modal-rf-set-community" placeholder="e.g. 65001:200 or no-export" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
-            </div>
-            <div>
-              <label class="block font-medium text-slate-300 mb-1">Set Next Hop (IP)</label>
-              <input type="text" id="modal-rf-set-next-hop" placeholder="e.g. 192.168.1.1" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-1.5 text-white font-mono">
-            </div>
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1">Selection Rule Script *</label>
+          <textarea id="modal-rf-sr-rule" rows="3" required placeholder="if (protocol bgp &amp;&amp; dst in 10.0.0.0/8) { accept; }" class="w-full bg-slate-950 border border-slate-700 rounded-lg p-3 text-emerald-400 font-mono text-xs focus:outline-none"></textarea>
+        </div>
+
+        <div class="grid grid-cols-3 gap-3">
+          <div class="col-span-2">
+            <label class="block font-semibold text-slate-300 mb-1">Comment</label>
+            <input type="text" id="modal-rf-sr-comment" placeholder="Select rule note" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white">
+          </div>
+          <div class="flex items-center pt-6">
+            <label class="flex items-center space-x-2 text-slate-300 cursor-pointer">
+              <input type="checkbox" id="modal-rf-sr-disabled" class="rounded text-emerald-600 bg-slate-800 border-slate-700">
+              <span class="font-medium">Disabled (X)</span>
+            </label>
           </div>
         </div>
 
+        <div class="pt-4 border-t border-slate-800 flex justify-end space-x-2">
+          <button type="button" onclick="closeSelectRuleModal()" class="px-4 py-1.5 rounded-lg text-slate-400 hover:bg-slate-800">Cancel</button>
+          <button type="submit" class="px-5 py-1.5 rounded-lg font-bold bg-emerald-600 hover:bg-emerald-500 text-white shadow-lg shadow-emerald-500/20">OK</button>
+        </div>
+      </form>
+    </div>
+  </div>
+
+  <!-- ================= MODAL: COMMUNITY LIST (/routing filter community-list) ================= -->
+  <div id="modal-rf-community-list" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
+    <div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in">
+      <div class="px-6 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/60">
+        <div>
+          <h3 id="modal-rf-cl-title" class="text-sm font-bold text-white">BGP Community List</h3>
+          <p class="text-[11px] text-slate-400">Named community sets for matching in filter rules (/routing filter community-list)</p>
+        </div>
+        <button onclick="closeCommunityListModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
+      </div>
+
+      <form onsubmit="submitCommunityList(event)" class="p-6 space-y-4 text-xs">
+        <input type="hidden" id="modal-rf-cl-id">
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1">List Name *</label>
+          <input type="text" id="modal-rf-cl-name" required placeholder="e.g. awmn-communities, blackhole" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+        </div>
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1">Communities (comma-separated) *</label>
+          <textarea id="modal-rf-cl-communities" rows="3" required placeholder="65000:100, 65000:200, no-export" class="w-full bg-slate-950 border border-slate-700 rounded-lg p-3 text-cyan-300 font-mono text-xs"></textarea>
+          <p class="text-[10px] text-slate-500 mt-1">Format: AS:value (e.g. 65000:100) or well-known keywords (no-export, blackhole)</p>
+        </div>
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1">RegExp (Optional)</label>
+          <input type="text" id="modal-rf-cl-regexp" placeholder="e.g. ^65000:" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+        </div>
+
         <div class="grid grid-cols-3 gap-3">
           <div class="col-span-2">
-            <label class="block font-semibold text-slate-300 mb-1.5">Comment</label>
-            <input type="text" id="modal-rf-comment" placeholder="Filter upstream customer prefix" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white">
+            <label class="block font-semibold text-slate-300 mb-1">Comment</label>
+            <input type="text" id="modal-rf-cl-comment" placeholder="Community list note" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white">
           </div>
           <div class="flex items-center pt-6">
             <label class="flex items-center space-x-2 text-slate-300 cursor-pointer">
-              <input type="checkbox" id="modal-rf-disabled" class="rounded text-emerald-600 bg-slate-800 border-slate-700">
+              <input type="checkbox" id="modal-rf-cl-disabled" class="rounded text-emerald-600 bg-slate-800 border-slate-700">
               <span class="font-medium">Disabled (X)</span>
             </label>
           </div>
         </div>
 
-        <div class="pt-4 border-t border-slate-800 flex justify-end space-x-3">
-          <button type="button" onclick="closeRoutingFilterModal()" class="px-4 py-2 rounded-lg text-slate-400 hover:bg-slate-800">Cancel</button>
-          <button type="submit" class="px-5 py-2 rounded-lg font-semibold bg-emerald-600 hover:bg-emerald-500 text-white shadow-lg shadow-emerald-500/20">Save Filter Rule</button>
+        <div class="pt-4 border-t border-slate-800 flex justify-end space-x-2">
+          <button type="button" onclick="closeCommunityListModal()" class="px-4 py-1.5 rounded-lg text-slate-400 hover:bg-slate-800">Cancel</button>
+          <button type="submit" class="px-5 py-1.5 rounded-lg font-bold bg-emerald-600 hover:bg-emerald-500 text-white shadow-lg shadow-emerald-500/20">OK</button>
+        </div>
+      </form>
+    </div>
+  </div>
+
+  <!-- ================= MODAL: AS LIST (/routing filter num-list) ================= -->
+  <div id="modal-rf-as-list" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">
+    <div class="bg-slate-900 border border-slate-800 rounded-2xl w-full max-w-lg shadow-2xl overflow-hidden animate-in">
+      <div class="px-6 py-4 border-b border-slate-800 flex items-center justify-between bg-slate-950/60">
+        <div>
+          <h3 id="modal-rf-al-title" class="text-sm font-bold text-white">AS Number List</h3>
+          <p class="text-[11px] text-slate-400">Named AS numbers or numeric ranges for AS-Path matching (/routing filter num-list)</p>
+        </div>
+        <button onclick="closeASListModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
+      </div>
+
+      <form onsubmit="submitASList(event)" class="p-6 space-y-4 text-xs">
+        <input type="hidden" id="modal-rf-al-id">
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1">List Name *</label>
+          <input type="text" id="modal-rf-al-name" required placeholder="e.g. tier1-asns, my-peers" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+        </div>
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1">Range / AS Numbers *</label>
+          <input type="text" id="modal-rf-al-range" required placeholder="e.g. 65000-65010, 6610, 17244" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono">
+          <p class="text-[10px] text-slate-500 mt-1">Specify numeric range (e.g. 65000-65010) or comma-separated ASNs</p>
+        </div>
+
+        <div class="grid grid-cols-3 gap-3">
+          <div class="col-span-2">
+            <label class="block font-semibold text-slate-300 mb-1">Comment</label>
+            <input type="text" id="modal-rf-al-comment" placeholder="AS list note" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white">
+          </div>
+          <div class="flex items-center pt-6">
+            <label class="flex items-center space-x-2 text-slate-300 cursor-pointer">
+              <input type="checkbox" id="modal-rf-al-disabled" class="rounded text-emerald-600 bg-slate-800 border-slate-700">
+              <span class="font-medium">Disabled (X)</span>
+            </label>
+          </div>
+        </div>
+
+        <div class="pt-4 border-t border-slate-800 flex justify-end space-x-2">
+          <button type="button" onclick="closeASListModal()" class="px-4 py-1.5 rounded-lg text-slate-400 hover:bg-slate-800">Cancel</button>
+          <button type="submit" class="px-5 py-1.5 rounded-lg font-bold bg-emerald-600 hover:bg-emerald-500 text-white shadow-lg shadow-emerald-500/20">OK</button>
         </div>
       </form>
     </div>
   </div>
+    </div>
+  </div>
 
   <!-- ================= MODAL: ADD / EDIT DHCP CLIENT ================= -->
   <div id="modal-dhcp-client" class="fixed inset-0 bg-slate-950/80 backdrop-blur-sm z-50 hidden flex items-center justify-center p-4">