Explorar o código

feat(dhcp): implement full MikroTik-style DHCP Server with NTP, TFTP, PXE boot, custom options, static leases and setup wizard

Proxmox Router hai 1 semana
pai
achega
5f06d1a2c5

+ 2 - 1
cmd/server/main.go

@@ -39,9 +39,10 @@ func main() {
 	bridgeMgr := bridge.NewBridgeManager()
 	toolsMgr := tools.NewManager()
 	dhcpMgr := dhcp.NewManager()
+	dhcpServerMgr := dhcp.NewServerManager()
 	webHandler := web.Handler()
 
-	srv := api.NewServer(netMgr, persistMgr, metaMgr, vpnMgr, fwMgr, routingMgr, dynRoutingMgr, bridgeMgr, toolsMgr, dhcpMgr, webHandler)
+	srv := api.NewServer(netMgr, persistMgr, metaMgr, vpnMgr, fwMgr, routingMgr, dynRoutingMgr, bridgeMgr, toolsMgr, dhcpMgr, dhcpServerMgr, webHandler)
 
 	httpServer := &http.Server{
 		Addr:         *addr,

+ 21 - 1
internal/api/api.go

@@ -27,11 +27,12 @@ type Server struct {
 	bridgeMgr     *bridge.BridgeManager
 	toolsMgr      *tools.Manager
 	dhcpMgr       *dhcp.Manager
+	dhcpServerMgr *dhcp.ServerManager
 	mux           *http.ServeMux
 	fsHandler     http.Handler
 }
 
-func NewServer(netMgr *network.Manager, persistMgr *network.PersistenceManager, metaMgr *network.MetadataManager, vpnMgr *vpn.Manager, fwMgr *firewall.FirewallManager, routingMgr *routing.RoutingManager, dynRoutingMgr *dynamicrouting.Manager, bridgeMgr *bridge.BridgeManager, toolsMgr *tools.Manager, dhcpMgr *dhcp.Manager, fsHandler http.Handler) *Server {
+func NewServer(netMgr *network.Manager, persistMgr *network.PersistenceManager, metaMgr *network.MetadataManager, vpnMgr *vpn.Manager, fwMgr *firewall.FirewallManager, routingMgr *routing.RoutingManager, dynRoutingMgr *dynamicrouting.Manager, bridgeMgr *bridge.BridgeManager, toolsMgr *tools.Manager, dhcpMgr *dhcp.Manager, dhcpServerMgr *dhcp.ServerManager, fsHandler http.Handler) *Server {
 	s := &Server{
 		netMgr:        netMgr,
 		persistMgr:    persistMgr,
@@ -43,6 +44,7 @@ func NewServer(netMgr *network.Manager, persistMgr *network.PersistenceManager,
 		bridgeMgr:     bridgeMgr,
 		toolsMgr:      toolsMgr,
 		dhcpMgr:       dhcpMgr,
+		dhcpServerMgr: dhcpServerMgr,
 		mux:           http.NewServeMux(),
 		fsHandler:     fsHandler,
 	}
@@ -342,6 +344,24 @@ func (s *Server) registerRoutes() {
 	s.mux.HandleFunc("POST /api/dhcp/clients/{id}/renew", s.handleRenewDHCPClient)
 	s.mux.HandleFunc("POST /api/dhcp/clients/{id}/release", s.handleReleaseDHCPClient)
 
+	// DHCP Server (/ip dhcp-server)
+	s.mux.HandleFunc("GET /api/dhcp-server/summary", s.handleGetDHCPServerSummary)
+	s.mux.HandleFunc("GET /api/dhcp-server/servers", s.handleListDHCPServers)
+	s.mux.HandleFunc("POST /api/dhcp-server/servers", s.handleSaveDHCPServer)
+	s.mux.HandleFunc("DELETE /api/dhcp-server/servers/{id}", s.handleDeleteDHCPServer)
+	s.mux.HandleFunc("POST /api/dhcp-server/servers/{id}/state", s.handleSetDHCPServerState)
+	s.mux.HandleFunc("GET /api/dhcp-server/networks", s.handleListDHCPNetworks)
+	s.mux.HandleFunc("POST /api/dhcp-server/networks", s.handleSaveDHCPNetwork)
+	s.mux.HandleFunc("DELETE /api/dhcp-server/networks/{id}", s.handleDeleteDHCPNetwork)
+	s.mux.HandleFunc("GET /api/dhcp-server/leases", s.handleListDHCPLeases)
+	s.mux.HandleFunc("POST /api/dhcp-server/leases/static", s.handleSaveDHCPStaticLease)
+	s.mux.HandleFunc("DELETE /api/dhcp-server/leases/{id}", s.handleDeleteDHCPStaticLease)
+	s.mux.HandleFunc("POST /api/dhcp-server/leases/make-static", s.handleMakeDHCPStaticLease)
+	s.mux.HandleFunc("GET /api/dhcp-server/options", s.handleListDHCPOptions)
+	s.mux.HandleFunc("POST /api/dhcp-server/options", s.handleSaveDHCPOption)
+	s.mux.HandleFunc("DELETE /api/dhcp-server/options/{id}", s.handleDeleteDHCPOption)
+	s.mux.HandleFunc("POST /api/dhcp-server/setup", s.handleRunDHCPSetup)
+
 	// Web UI Static Files
 	if s.fsHandler != nil {
 		s.mux.Handle("/", s.fsHandler)

+ 235 - 0
internal/api/dhcp_server_handlers.go

@@ -0,0 +1,235 @@
+package api
+
+import (
+	"encoding/json"
+	"net/http"
+	"proxmox-router/internal/dhcp"
+)
+
+func (s *Server) handleGetDHCPServerSummary(w http.ResponseWriter, r *http.Request) {
+	summary := s.dhcpServerMgr.GetSummary()
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(summary)
+}
+
+func (s *Server) handleListDHCPServers(w http.ResponseWriter, r *http.Request) {
+	servers := s.dhcpServerMgr.ListServers()
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(servers)
+}
+
+func (s *Server) handleSaveDHCPServer(w http.ResponseWriter, r *http.Request) {
+	var inst dhcp.DHCPServerInstance
+	if err := json.NewDecoder(r.Body).Decode(&inst); err != nil {
+		http.Error(w, "invalid request body", http.StatusBadRequest)
+		return
+	}
+
+	saved, err := s.dhcpServerMgr.SaveServer(inst)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(saved)
+}
+
+func (s *Server) handleDeleteDHCPServer(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		http.Error(w, "missing server ID", http.StatusBadRequest)
+		return
+	}
+
+	if err := s.dhcpServerMgr.DeleteServer(id); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
+}
+
+func (s *Server) handleSetDHCPServerState(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		http.Error(w, "missing server ID", http.StatusBadRequest)
+		return
+	}
+
+	var req struct {
+		Disabled bool `json:"disabled"`
+	}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		http.Error(w, "invalid JSON", http.StatusBadRequest)
+		return
+	}
+
+	if err := s.dhcpServerMgr.ToggleServerState(id, req.Disabled); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
+}
+
+// ======================== NETWORKS ========================
+func (s *Server) handleListDHCPNetworks(w http.ResponseWriter, r *http.Request) {
+	networks := s.dhcpServerMgr.ListNetworks()
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(networks)
+}
+
+func (s *Server) handleSaveDHCPNetwork(w http.ResponseWriter, r *http.Request) {
+	var netw dhcp.DHCPNetwork
+	if err := json.NewDecoder(r.Body).Decode(&netw); err != nil {
+		http.Error(w, "invalid request body", http.StatusBadRequest)
+		return
+	}
+
+	saved, err := s.dhcpServerMgr.SaveNetwork(netw)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(saved)
+}
+
+func (s *Server) handleDeleteDHCPNetwork(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		http.Error(w, "missing network ID", http.StatusBadRequest)
+		return
+	}
+
+	if err := s.dhcpServerMgr.DeleteNetwork(id); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
+}
+
+// ======================== LEASES ========================
+func (s *Server) handleListDHCPLeases(w http.ResponseWriter, r *http.Request) {
+	leases := s.dhcpServerMgr.ListLeases()
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(leases)
+}
+
+func (s *Server) handleSaveDHCPStaticLease(w http.ResponseWriter, r *http.Request) {
+	var lease dhcp.DHCPLease
+	if err := json.NewDecoder(r.Body).Decode(&lease); err != nil {
+		http.Error(w, "invalid request body", http.StatusBadRequest)
+		return
+	}
+
+	saved, err := s.dhcpServerMgr.SaveStaticLease(lease)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(saved)
+}
+
+func (s *Server) handleDeleteDHCPStaticLease(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		http.Error(w, "missing lease ID", http.StatusBadRequest)
+		return
+	}
+
+	if err := s.dhcpServerMgr.DeleteStaticLease(id); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
+}
+
+func (s *Server) handleMakeDHCPStaticLease(w http.ResponseWriter, r *http.Request) {
+	var req struct {
+		MACAddress string `json:"mac_address"`
+		Address    string `json:"address"`
+		HostName   string `json:"hostname"`
+		Comment    string `json:"comment"`
+	}
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		http.Error(w, "invalid JSON", http.StatusBadRequest)
+		return
+	}
+
+	st, err := s.dhcpServerMgr.MakeStaticLease(req.MACAddress, req.Address, req.HostName, req.Comment)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(st)
+}
+
+// ======================== OPTIONS ========================
+func (s *Server) handleListDHCPOptions(w http.ResponseWriter, r *http.Request) {
+	options := s.dhcpServerMgr.ListOptions()
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(options)
+}
+
+func (s *Server) handleSaveDHCPOption(w http.ResponseWriter, r *http.Request) {
+	var opt dhcp.DHCPOption
+	if err := json.NewDecoder(r.Body).Decode(&opt); err != nil {
+		http.Error(w, "invalid request body", http.StatusBadRequest)
+		return
+	}
+
+	saved, err := s.dhcpServerMgr.SaveOption(opt)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(saved)
+}
+
+func (s *Server) handleDeleteDHCPOption(w http.ResponseWriter, r *http.Request) {
+	id := r.PathValue("id")
+	if id == "" {
+		http.Error(w, "missing option ID", http.StatusBadRequest)
+		return
+	}
+
+	if err := s.dhcpServerMgr.DeleteOption(id); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
+}
+
+// ======================== WIZARD ========================
+func (s *Server) handleRunDHCPSetup(w http.ResponseWriter, r *http.Request) {
+	var req dhcp.DHCPServerSetupRequest
+	if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+		http.Error(w, "invalid request body", http.StatusBadRequest)
+		return
+	}
+
+	if err := s.dhcpServerMgr.RunSetupWizard(req); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json")
+	_ = json.NewEncoder(w).Encode(map[string]bool{"success": true})
+}

+ 793 - 0
internal/dhcp/server_manager.go

@@ -0,0 +1,793 @@
+package dhcp
+
+import (
+	"encoding/json"
+	"fmt"
+	"log"
+	"net"
+	"os"
+	"os/exec"
+	"path/filepath"
+	"strconv"
+	"strings"
+	"sync"
+	"time"
+)
+
+const (
+	DHCPServerStoreFile = "/etc/proxmox-router/dhcp_server.json"
+	DnsmasqConfFile     = "/etc/dnsmasq.d/debtik_dhcp.conf"
+	DnsmasqLeasesFile   = "/var/lib/misc/dnsmasq.leases"
+)
+
+type ServerManager struct {
+	mu           sync.RWMutex
+	storePath    string
+	confPath     string
+	leasesPath   string
+	servers      map[string]DHCPServerInstance
+	networks     map[string]DHCPNetwork
+	staticLeases map[string]DHCPLease
+	options      map[string]DHCPOption
+}
+
+func NewServerManager() *ServerManager {
+	sm := &ServerManager{
+		storePath:    DHCPServerStoreFile,
+		confPath:     DnsmasqConfFile,
+		leasesPath:   DnsmasqLeasesFile,
+		servers:      make(map[string]DHCPServerInstance),
+		networks:     make(map[string]DHCPNetwork),
+		staticLeases: make(map[string]DHCPLease),
+		options:      make(map[string]DHCPOption),
+	}
+
+	_ = os.MkdirAll(filepath.Dir(sm.storePath), 0755)
+	_ = os.MkdirAll(filepath.Dir(sm.confPath), 0755)
+
+	if err := sm.load(); err != nil {
+		log.Printf("[DHCP-Server] Error loading config: %v", err)
+	}
+
+	// Initial render and reload dnsmasq
+	if err := sm.applyDnsmasqConfigLocked(); err != nil {
+		log.Printf("[DHCP-Server] Error applying dnsmasq config: %v", err)
+	}
+
+	return sm
+}
+
+func (sm *ServerManager) load() error {
+	sm.mu.Lock()
+	defer sm.mu.Unlock()
+
+	data, err := os.ReadFile(sm.storePath)
+	if err != nil {
+		if os.IsNotExist(err) {
+			// Populate default option presets (MikroTik standard presets)
+			sm.initDefaultOptions()
+			return nil
+		}
+		return err
+	}
+
+	var store DHCPServerConfigStore
+	if err := json.Unmarshal(data, &store); err != nil {
+		return err
+	}
+
+	for _, s := range store.Servers {
+		sm.servers[s.ID] = s
+	}
+	for _, n := range store.Networks {
+		sm.networks[n.ID] = n
+	}
+	for _, l := range store.StaticLeases {
+		sm.staticLeases[l.ID] = l
+	}
+	for _, o := range store.Options {
+		sm.options[o.ID] = o
+	}
+
+	if len(sm.options) == 0 {
+		sm.initDefaultOptions()
+	}
+
+	return nil
+}
+
+func (sm *ServerManager) initDefaultOptions() {
+	presets := []DHCPOption{
+		{ID: "opt-ntp", Name: "ntp-servers", Code: 42, Value: "192.168.88.1", Comment: "NTP Time Servers (Option 42)"},
+		{ID: "opt-tftp-name", Name: "tftp-server-name", Code: 66, Value: "192.168.88.50", Comment: "TFTP Server Name / IP (Option 66)"},
+		{ID: "opt-bootfile", Name: "bootfile-name", Code: 67, Value: "pxelinux.0", Comment: "PXE Boot File (Option 67)"},
+		{ID: "opt-unifi", Name: "unifi-controller", Code: 43, Value: "01:04:c0:a8:58:0a", Comment: "UniFi Inform Controller (Option 43 Hex IP)"},
+		{ID: "opt-voip", Name: "cisco-voip-tftp", Code: 150, Value: "192.168.88.50", Comment: "Cisco VoIP TFTP (Option 150)"},
+		{ID: "opt-capwap", Name: "capwap-ac", Code: 138, Value: "192.168.88.10", Comment: "CAPWAP Access Controller (Option 138)"},
+		{ID: "opt-wpad", Name: "wpad-url", Code: 252, Value: "\"http://wpad/wpad.dat\"", Comment: "Web Proxy Auto-Discovery (Option 252)"},
+	}
+	for _, p := range presets {
+		sm.options[p.ID] = p
+	}
+}
+
+func (sm *ServerManager) saveLocked() error {
+	store := DHCPServerConfigStore{
+		Servers:      make([]DHCPServerInstance, 0, len(sm.servers)),
+		Networks:     make([]DHCPNetwork, 0, len(sm.networks)),
+		StaticLeases: make([]DHCPLease, 0, len(sm.staticLeases)),
+		Options:      make([]DHCPOption, 0, len(sm.options)),
+	}
+
+	for _, s := range sm.servers {
+		copyS := s
+		copyS.Running = false
+		copyS.ActiveLeases = 0
+		store.Servers = append(store.Servers, copyS)
+	}
+	for _, n := range sm.networks {
+		store.Networks = append(store.Networks, n)
+	}
+	for _, l := range sm.staticLeases {
+		store.StaticLeases = append(store.StaticLeases, l)
+	}
+	for _, o := range sm.options {
+		store.Options = append(store.Options, o)
+	}
+
+	data, err := json.MarshalIndent(store, "", "  ")
+	if err != nil {
+		return err
+	}
+
+	return os.WriteFile(sm.storePath, data, 0644)
+}
+
+func (sm *ServerManager) applyDnsmasqConfigLocked() error {
+	var sb strings.Builder
+	sb.WriteString("# ==================================================================\n")
+	sb.WriteString("# DebTik DHCP Server Configuration (MikroTik RouterOS style)\n")
+	sb.WriteString("# Auto-generated by DebTik Network Appliance. DO NOT EDIT DIRECTLY.\n")
+	sb.WriteString("# ==================================================================\n\n")
+
+	sb.WriteString("dhcp-authoritative\n")
+	sb.WriteString("dhcp-leasefile=/var/lib/misc/dnsmasq.leases\n")
+	sb.WriteString("log-dhcp\n")
+	sb.WriteString("bind-dynamic\n\n")
+
+	activeServerCount := 0
+	for _, srv := range sm.servers {
+		if srv.Disabled {
+			continue
+		}
+		activeServerCount++
+		tag := sanitizeTag(srv.Name)
+		sb.WriteString(fmt.Sprintf("# --- DHCP Server: %s on %s ---\n", srv.Name, srv.Interface))
+		sb.WriteString(fmt.Sprintf("interface=%s\n", srv.Interface))
+
+		leaseTime := srv.LeaseTime
+		if leaseTime == "" {
+			leaseTime = "12h"
+		}
+
+		// Parse Address Pool range (e.g. 192.168.88.10-192.168.88.254)
+		startIP, endIP, netmask := parseAddressPool(srv.AddressPool)
+		if startIP != "" && endIP != "" {
+			if netmask != "" {
+				sb.WriteString(fmt.Sprintf("dhcp-range=set:%s,interface:%s,%s,%s,%s,%s\n", tag, srv.Interface, startIP, endIP, netmask, leaseTime))
+			} else {
+				sb.WriteString(fmt.Sprintf("dhcp-range=set:%s,interface:%s,%s,%s,%s\n", tag, srv.Interface, startIP, endIP, leaseTime))
+			}
+		}
+		sb.WriteString("\n")
+	}
+
+	// Subnet Networks & Options
+	sb.WriteString("# --- DHCP Networks & Options (/ip dhcp-server network) ---\n")
+	for _, netw := range sm.networks {
+		sb.WriteString(fmt.Sprintf("# Subnet: %s\n", netw.Address))
+		_, ipNet, err := net.ParseCIDR(netw.Address)
+		networkTag := ""
+		if err == nil && ipNet != nil {
+			networkTag = fmt.Sprintf("net_%s", strings.ReplaceAll(ipNet.IP.String(), ".", "_"))
+			// Match server range tag or subnet tag
+			sb.WriteString(fmt.Sprintf("dhcp-range=set:%s,%s,static\n", networkTag, ipNet.IP.String()))
+		}
+
+		// Determine target tag (either network tag or global/matched)
+		targetTag := ""
+		if networkTag != "" {
+			targetTag = fmt.Sprintf("tag:%s,", networkTag)
+		}
+
+		// Option 3: Gateway (Router)
+		if netw.Gateway != "" {
+			sb.WriteString(fmt.Sprintf("dhcp-option=%soption:router,%s\n", targetTag, netw.Gateway))
+		}
+
+		// Option 6: DNS Servers
+		if len(netw.DNSServers) > 0 {
+			sb.WriteString(fmt.Sprintf("dhcp-option=%soption:dns-server,%s\n", targetTag, strings.Join(netw.DNSServers, ",")))
+		}
+
+		// Option 15: Domain Name
+		if netw.Domain != "" {
+			sb.WriteString(fmt.Sprintf("dhcp-option=%soption:domain-name,%s\n", targetTag, netw.Domain))
+		}
+
+		// Option 42: NTP Servers (MikroTik /ip dhcp-server network ntp-server)
+		if len(netw.NTPServers) > 0 {
+			sb.WriteString(fmt.Sprintf("dhcp-option=%soption:ntp-server,%s\n", targetTag, strings.Join(netw.NTPServers, ",")))
+		}
+
+		// Option 66: TFTP Server Name / IP (next-server)
+		if netw.NextServer != "" {
+			sb.WriteString(fmt.Sprintf("dhcp-option=%soption:tftp-server,%s\n", targetTag, netw.NextServer))
+		}
+
+		// Option 67: Boot File Name (PXE)
+		if netw.BootFileName != "" {
+			sb.WriteString(fmt.Sprintf("dhcp-option=%soption:bootfile-name,%s\n", targetTag, netw.BootFileName))
+			if netw.NextServer != "" {
+				sb.WriteString(fmt.Sprintf("dhcp-boot=%s%s,,%s\n", targetTag, netw.BootFileName, netw.NextServer))
+			} else {
+				sb.WriteString(fmt.Sprintf("dhcp-boot=%s%s\n", targetTag, netw.BootFileName))
+			}
+		}
+
+		// Option 44: WINS Servers
+		if len(netw.WINSServers) > 0 {
+			sb.WriteString(fmt.Sprintf("dhcp-option=%soption:netbios-ns,%s\n", targetTag, strings.Join(netw.WINSServers, ",")))
+		}
+
+		// Linked custom DHCP options
+		for _, optID := range netw.DHCPOptions {
+			if opt, exists := sm.options[optID]; exists {
+				sb.WriteString(fmt.Sprintf("dhcp-option=%s%d,%s # %s\n", targetTag, opt.Code, opt.Value, opt.Name))
+			}
+		}
+		sb.WriteString("\n")
+	}
+
+	// Static Leases & Reservations (/ip dhcp-server lease)
+	sb.WriteString("# --- Static Leases & Reservations (/ip dhcp-server lease) ---\n")
+	for _, l := range sm.staticLeases {
+		if l.Disabled {
+			continue
+		}
+		mac := strings.ToLower(strings.TrimSpace(l.MACAddress))
+		ip := strings.TrimSpace(l.Address)
+		hostname := strings.TrimSpace(l.HostName)
+		if hostname == "" {
+			hostname = "reserved"
+		}
+		if mac != "" && ip != "" {
+			sb.WriteString(fmt.Sprintf("dhcp-host=%s,%s,%s,infinite\n", mac, ip, hostname))
+		}
+	}
+
+	// Write config to disk
+	if err := os.WriteFile(sm.confPath, []byte(sb.String()), 0644); err != nil {
+		return fmt.Errorf("failed to write dnsmasq config: %w", err)
+	}
+
+	// Restart or reload dnsmasq
+	if activeServerCount > 0 {
+		_ = exec.Command("systemctl", "restart", "dnsmasq").Run()
+	} else {
+		_ = exec.Command("systemctl", "reload", "dnsmasq").Run()
+	}
+
+	return nil
+}
+
+func (sm *ServerManager) ListServers() []DHCPServerInstance {
+	sm.mu.RLock()
+	defer sm.mu.RUnlock()
+
+	activeLeases := sm.countActiveLeasesByServer()
+
+	list := make([]DHCPServerInstance, 0, len(sm.servers))
+	for _, s := range sm.servers {
+		copyS := s
+		copyS.Running = !s.Disabled && isInterfaceUp(s.Interface)
+		copyS.ActiveLeases = activeLeases[s.Name]
+		list = append(list, copyS)
+	}
+	return list
+}
+
+func (sm *ServerManager) GetServer(id string) (DHCPServerInstance, bool) {
+	sm.mu.RLock()
+	defer sm.mu.RUnlock()
+
+	s, ok := sm.servers[id]
+	if !ok {
+		return DHCPServerInstance{}, false
+	}
+	s.Running = !s.Disabled && isInterfaceUp(s.Interface)
+	return s, true
+}
+
+func (sm *ServerManager) SaveServer(inst DHCPServerInstance) (DHCPServerInstance, error) {
+	sm.mu.Lock()
+	defer sm.mu.Unlock()
+
+	if inst.ID == "" {
+		inst.ID = fmt.Sprintf("dhcp-%d", time.Now().UnixNano())
+	}
+	if inst.Name == "" {
+		inst.Name = fmt.Sprintf("dhcp%d", len(sm.servers)+1)
+	}
+	if inst.LeaseTime == "" {
+		inst.LeaseTime = "12h"
+	}
+	inst.Authoritative = true
+
+	sm.servers[inst.ID] = inst
+	if err := sm.saveLocked(); err != nil {
+		return inst, err
+	}
+
+	_ = sm.applyDnsmasqConfigLocked()
+	return inst, nil
+}
+
+func (sm *ServerManager) DeleteServer(id string) error {
+	sm.mu.Lock()
+	defer sm.mu.Unlock()
+
+	delete(sm.servers, id)
+	if err := sm.saveLocked(); err != nil {
+		return err
+	}
+
+	return sm.applyDnsmasqConfigLocked()
+}
+
+func (sm *ServerManager) ToggleServerState(id string, disabled bool) error {
+	sm.mu.Lock()
+	defer sm.mu.Unlock()
+
+	s, exists := sm.servers[id]
+	if !exists {
+		return fmt.Errorf("server not found: %s", id)
+	}
+
+	s.Disabled = disabled
+	sm.servers[id] = s
+	if err := sm.saveLocked(); err != nil {
+		return err
+	}
+
+	return sm.applyDnsmasqConfigLocked()
+}
+
+// ======================== NETWORKS ========================
+func (sm *ServerManager) ListNetworks() []DHCPNetwork {
+	sm.mu.RLock()
+	defer sm.mu.RUnlock()
+
+	list := make([]DHCPNetwork, 0, len(sm.networks))
+	for _, n := range sm.networks {
+		list = append(list, n)
+	}
+	return list
+}
+
+func (sm *ServerManager) SaveNetwork(netw DHCPNetwork) (DHCPNetwork, error) {
+	sm.mu.Lock()
+	defer sm.mu.Unlock()
+
+	if netw.ID == "" {
+		netw.ID = fmt.Sprintf("net-%d", time.Now().UnixNano())
+	}
+	if netw.Address == "" {
+		return netw, fmt.Errorf("subnet address (CIDR) is required")
+	}
+
+	sm.networks[netw.ID] = netw
+	if err := sm.saveLocked(); err != nil {
+		return netw, err
+	}
+
+	_ = sm.applyDnsmasqConfigLocked()
+	return netw, nil
+}
+
+func (sm *ServerManager) DeleteNetwork(id string) error {
+	sm.mu.Lock()
+	defer sm.mu.Unlock()
+
+	delete(sm.networks, id)
+	if err := sm.saveLocked(); err != nil {
+		return err
+	}
+
+	return sm.applyDnsmasqConfigLocked()
+}
+
+// ======================== LEASES ========================
+func (sm *ServerManager) ListLeases() []DHCPLease {
+	sm.mu.RLock()
+	defer sm.mu.RUnlock()
+
+	dynamicLeases := sm.readDnsmasqLeasesFile()
+	staticMap := make(map[string]DHCPLease)
+	for _, st := range sm.staticLeases {
+		staticMap[strings.ToLower(st.MACAddress)] = st
+	}
+
+	result := make([]DHCPLease, 0)
+	seenMACs := make(map[string]bool)
+
+	// Combine dynamic leases
+	for _, dl := range dynamicLeases {
+		macLower := strings.ToLower(dl.MACAddress)
+		seenMACs[macLower] = true
+
+		if st, isStatic := staticMap[macLower]; isStatic {
+			dl.Dynamic = false
+			dl.Status = "static"
+			dl.Comment = st.Comment
+			dl.Disabled = st.Disabled
+			dl.ExpiresIn = "never (static)"
+		} else {
+			dl.Dynamic = true
+			dl.Status = "bound"
+		}
+		result = append(result, dl)
+	}
+
+	// Add static reservations that haven't acquired lease yet
+	for macLower, st := range staticMap {
+		if !seenMACs[macLower] {
+			st.Dynamic = false
+			st.Status = "waiting"
+			st.ExpiresIn = "never (static)"
+			result = append(result, st)
+		}
+	}
+
+	return result
+}
+
+func (sm *ServerManager) readDnsmasqLeasesFile() []DHCPLease {
+	data, err := os.ReadFile(sm.leasesPath)
+	if err != nil {
+		return nil
+	}
+
+	lines := strings.Split(string(data), "\n")
+	leases := make([]DHCPLease, 0, len(lines))
+
+	now := time.Now()
+	for _, line := range lines {
+		line = strings.TrimSpace(line)
+		if line == "" {
+			continue
+		}
+		// Format: <timestamp> <mac> <ip> <hostname> <client-id>
+		parts := strings.Fields(line)
+		if len(parts) < 3 {
+			continue
+		}
+
+		expTimestamp, _ := strconv.ParseInt(parts[0], 10, 64)
+		mac := parts[1]
+		ip := parts[2]
+		hostname := "*"
+		if len(parts) >= 4 && parts[3] != "*" {
+			hostname = parts[3]
+		}
+		clientID := ""
+		if len(parts) >= 5 && parts[4] != "*" {
+			clientID = parts[4]
+		}
+
+		expiresAt := time.Unix(expTimestamp, 0)
+		expiresIn := "expired"
+		if expiresAt.After(now) {
+			dur := expiresAt.Sub(now).Truncate(time.Minute)
+			hours := int(dur.Hours())
+			mins := int(dur.Minutes()) % 60
+			expiresIn = fmt.Sprintf("%dh %dm", hours, mins)
+		}
+
+		// Determine server by matching IP against configured networks/pools
+		serverName := sm.matchServerForIP(ip)
+
+		leases = append(leases, DHCPLease{
+			ID:         fmt.Sprintf("lease-%s", strings.ReplaceAll(mac, ":", "")),
+			Address:    ip,
+			MACAddress: mac,
+			HostName:   hostname,
+			ClientID:   clientID,
+			Server:     serverName,
+			ExpiresAt:  expiresAt,
+			ExpiresIn:  expiresIn,
+			Dynamic:    true,
+			Status:     "bound",
+		})
+	}
+
+	return leases
+}
+
+func (sm *ServerManager) matchServerForIP(ipStr string) string {
+	targetIP := net.ParseIP(ipStr)
+	if targetIP == nil {
+		return "dhcp1"
+	}
+
+	for _, srv := range sm.servers {
+		startIP, endIP, _ := parseAddressPool(srv.AddressPool)
+		if startIP != "" && endIP != "" {
+			s := net.ParseIP(startIP)
+			e := net.ParseIP(endIP)
+			if s != nil && e != nil && compareIPs(targetIP, s) >= 0 && compareIPs(targetIP, e) <= 0 {
+				return srv.Name
+			}
+		}
+	}
+	return "dhcp1"
+}
+
+func (sm *ServerManager) countActiveLeasesByServer() map[string]int {
+	counts := make(map[string]int)
+	leases := sm.readDnsmasqLeasesFile()
+	for _, l := range leases {
+		counts[l.Server]++
+	}
+	return counts
+}
+
+func (sm *ServerManager) SaveStaticLease(lease DHCPLease) (DHCPLease, error) {
+	sm.mu.Lock()
+	defer sm.mu.Unlock()
+
+	if lease.ID == "" {
+		lease.ID = fmt.Sprintf("static-%d", time.Now().UnixNano())
+	}
+	if lease.MACAddress == "" || lease.Address == "" {
+		return lease, fmt.Errorf("both MAC address and IP address are required")
+	}
+
+	lease.MACAddress = strings.ToLower(lease.MACAddress)
+	lease.Dynamic = false
+	lease.Status = "static"
+
+	sm.staticLeases[lease.ID] = lease
+	if err := sm.saveLocked(); err != nil {
+		return lease, err
+	}
+
+	_ = sm.applyDnsmasqConfigLocked()
+	return lease, nil
+}
+
+func (sm *ServerManager) DeleteStaticLease(id string) error {
+	sm.mu.Lock()
+	defer sm.mu.Unlock()
+
+	delete(sm.staticLeases, id)
+	if err := sm.saveLocked(); err != nil {
+		return err
+	}
+
+	return sm.applyDnsmasqConfigLocked()
+}
+
+func (sm *ServerManager) MakeStaticLease(mac, ip, hostname, comment string) (DHCPLease, error) {
+	sm.mu.Lock()
+	defer sm.mu.Unlock()
+
+	macLower := strings.ToLower(strings.TrimSpace(mac))
+	// Check if already in static leases
+	for _, st := range sm.staticLeases {
+		if strings.ToLower(st.MACAddress) == macLower {
+			st.Address = ip
+			if hostname != "" && hostname != "*" {
+				st.HostName = hostname
+			}
+			if comment != "" {
+				st.Comment = comment
+			}
+			sm.staticLeases[st.ID] = st
+			_ = sm.saveLocked()
+			_ = sm.applyDnsmasqConfigLocked()
+			return st, nil
+		}
+	}
+
+	id := fmt.Sprintf("static-%d", time.Now().UnixNano())
+	st := DHCPLease{
+		ID:         id,
+		MACAddress: macLower,
+		Address:    ip,
+		HostName:   hostname,
+		Server:     "all",
+		Comment:    comment,
+		Dynamic:    false,
+		Status:     "static",
+	}
+
+	sm.staticLeases[id] = st
+	if err := sm.saveLocked(); err != nil {
+		return st, err
+	}
+
+	_ = sm.applyDnsmasqConfigLocked()
+	return st, nil
+}
+
+// ======================== OPTIONS ========================
+func (sm *ServerManager) ListOptions() []DHCPOption {
+	sm.mu.RLock()
+	defer sm.mu.RUnlock()
+
+	list := make([]DHCPOption, 0, len(sm.options))
+	for _, o := range sm.options {
+		list = append(list, o)
+	}
+	return list
+}
+
+func (sm *ServerManager) SaveOption(opt DHCPOption) (DHCPOption, error) {
+	sm.mu.Lock()
+	defer sm.mu.Unlock()
+
+	if opt.ID == "" {
+		opt.ID = fmt.Sprintf("opt-%d", time.Now().UnixNano())
+	}
+	if opt.Name == "" || opt.Code <= 0 || opt.Code > 254 {
+		return opt, fmt.Errorf("valid option name and code (1-254) are required")
+	}
+
+	sm.options[opt.ID] = opt
+	if err := sm.saveLocked(); err != nil {
+		return opt, err
+	}
+
+	_ = sm.applyDnsmasqConfigLocked()
+	return opt, nil
+}
+
+func (sm *ServerManager) DeleteOption(id string) error {
+	sm.mu.Lock()
+	defer sm.mu.Unlock()
+
+	delete(sm.options, id)
+	if err := sm.saveLocked(); err != nil {
+		return err
+	}
+
+	return sm.applyDnsmasqConfigLocked()
+}
+
+// ======================== SETUP WIZARD (MikroTik style) ========================
+func (sm *ServerManager) RunSetupWizard(req DHCPServerSetupRequest) error {
+	sm.mu.Lock()
+	defer sm.mu.Unlock()
+
+	if req.Interface == "" {
+		return fmt.Errorf("interface is required")
+	}
+	if req.AddressSpace == "" {
+		req.AddressSpace = "192.168.88.0/24"
+	}
+	if req.Gateway == "" {
+		req.Gateway = "192.168.88.1"
+	}
+	if req.AddressPool == "" {
+		req.AddressPool = "192.168.88.10-192.168.88.254"
+	}
+	if len(req.DNSServers) == 0 {
+		req.DNSServers = []string{req.Gateway, "1.1.1.1"}
+	}
+	if req.LeaseTime == "" {
+		req.LeaseTime = "12h"
+	}
+
+	srvID := fmt.Sprintf("dhcp-%s", req.Interface)
+	srv := DHCPServerInstance{
+		ID:            srvID,
+		Name:          fmt.Sprintf("dhcp-%s", req.Interface),
+		Interface:     req.Interface,
+		AddressPool:   req.AddressPool,
+		LeaseTime:     req.LeaseTime,
+		Authoritative: true,
+		Disabled:      false,
+		Comment:       "Created by DHCP Setup Wizard",
+	}
+	sm.servers[srvID] = srv
+
+	netID := fmt.Sprintf("net-%s", strings.ReplaceAll(req.AddressSpace, "/", "_"))
+	netw := DHCPNetwork{
+		ID:           netID,
+		Address:      req.AddressSpace,
+		Gateway:      req.Gateway,
+		DNSServers:   req.DNSServers,
+		Domain:       req.Domain,
+		NTPServers:   req.NTPServers,
+		NextServer:   req.NextServer,
+		BootFileName: req.BootFileName,
+		Comment:      fmt.Sprintf("Network for %s", req.Interface),
+	}
+	sm.networks[netID] = netw
+
+	if err := sm.saveLocked(); err != nil {
+		return err
+	}
+
+	return sm.applyDnsmasqConfigLocked()
+}
+
+// ======================== SUMMARY ========================
+func (sm *ServerManager) GetSummary() DHCPServerSummary {
+	sm.mu.RLock()
+	defer sm.mu.RUnlock()
+
+	summary := DHCPServerSummary{
+		TotalServers:  len(sm.servers),
+		TotalNetworks: len(sm.networks),
+		StaticLeases:  len(sm.staticLeases),
+		TotalOptions:  len(sm.options),
+	}
+
+	for _, s := range sm.servers {
+		if !s.Disabled && isInterfaceUp(s.Interface) {
+			summary.ActiveServers++
+		}
+	}
+
+	leases := sm.readDnsmasqLeasesFile()
+	summary.ActiveLeases = len(leases)
+
+	return summary
+}
+
+// ======================== HELPERS ========================
+func sanitizeTag(name string) string {
+	res := strings.Map(func(r rune) rune {
+		if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' {
+			return r
+		}
+		return '_'
+	}, name)
+	return strings.ToLower(res)
+}
+
+func parseAddressPool(pool string) (startIP, endIP, netmask string) {
+	pool = strings.TrimSpace(pool)
+	parts := strings.Split(pool, "-")
+	if len(parts) == 2 {
+		return strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1]), ""
+	}
+	return "", "", ""
+}
+
+func isInterfaceUp(ifaceName string) bool {
+	iface, err := net.InterfaceByName(ifaceName)
+	if err != nil {
+		return false
+	}
+	return (iface.Flags & net.FlagUp) != 0
+}
+
+func compareIPs(a, b net.IP) int {
+	a4 := a.To4()
+	b4 := b.To4()
+	if a4 == nil || b4 == nil {
+		return 0
+	}
+	for i := 0; i < 4; i++ {
+		if a4[i] < b4[i] {
+			return -1
+		}
+		if a4[i] > b4[i] {
+			return 1
+		}
+	}
+	return 0
+}

+ 92 - 0
internal/dhcp/server_types.go

@@ -0,0 +1,92 @@
+package dhcp
+
+import "time"
+
+// DHCPServerInstance represents an interface-bound DHCP server (/ip dhcp-server)
+type DHCPServerInstance struct {
+	ID            string `json:"id"`
+	Name          string `json:"name"`           // e.g. "dhcp1", "defconf"
+	Interface     string `json:"interface"`      // e.g. "bridge1", "eth0"
+	LeaseTime     string `json:"lease_time"`     // e.g. "12h", "1d", "30m"
+	AddressPool   string `json:"address_pool"`   // e.g. "192.168.88.10-192.168.88.254"
+	Authoritative bool   `json:"authoritative"`  // default true
+	Disabled      bool   `json:"disabled"`       // MikroTik 'X' flag
+	Comment       string `json:"comment,omitempty"`
+
+	// Runtime telemetry
+	Running      bool `json:"running"`
+	ActiveLeases int  `json:"active_leases"`
+}
+
+// DHCPNetwork defines options and gateway for a specific subnet (/ip dhcp-server network)
+type DHCPNetwork struct {
+	ID           string   `json:"id"`
+	Address      string   `json:"address"`       // CIDR e.g. "192.168.88.0/24"
+	Gateway      string   `json:"gateway"`       // Option 3 (Router IP)
+	Netmask      string   `json:"netmask"`       // Netmask e.g. "255.255.255.0" or "24"
+	DNSServers   []string `json:"dns_servers"`   // Option 6
+	Domain       string   `json:"domain"`        // Option 15
+	NTPServers   []string `json:"ntp_servers"`   // Option 42 (requested by user)
+	NextServer   string   `json:"next_server"`   // Option 66 / TFTP server IP/hostname (requested by user)
+	BootFileName string   `json:"boot_file_name"`// Option 67 / PXE boot filename
+	WINSServers  []string `json:"wins_servers"`  // Option 44
+	DHCPOptions  []string `json:"dhcp_options"`  // Linked custom option IDs
+	Comment      string   `json:"comment,omitempty"`
+}
+
+// DHCPLease represents an active dynamic lease or static reservation (/ip dhcp-server lease)
+type DHCPLease struct {
+	ID         string    `json:"id"`
+	Address    string    `json:"address"`     // Assigned IP
+	MACAddress string    `json:"mac_address"` // Format "aa:bb:cc:dd:ee:ff"
+	ClientID   string    `json:"client_id,omitempty"`
+	Server     string    `json:"server"`      // Server name or "all"
+	HostName   string    `json:"hostname"`
+	ExpiresIn  string    `json:"expires_in"`  // e.g. "11h 45m" or "never"
+	ExpiresAt  time.Time `json:"expires_at"`
+	Dynamic    bool      `json:"dynamic"`     // true=from dnsmasq.leases, false=static reservation
+	Status     string    `json:"status"`      // "bound", "static", "waiting"
+	Disabled   bool      `json:"disabled"`
+	Comment    string    `json:"comment,omitempty"`
+}
+
+// DHCPOption represents a raw or typed custom DHCP option (/ip dhcp-server option)
+type DHCPOption struct {
+	ID      string `json:"id"`
+	Name    string `json:"name"`    // e.g. "unifi-controller", "voip-tftp"
+	Code    int    `json:"code"`    // Option code (1 - 254)
+	Value   string `json:"value"`   // Raw IP, text, hex or option string
+	Comment string `json:"comment,omitempty"`
+}
+
+// DHCPServerSummary provides aggregated statistics for KPI cards & sidebar badges
+type DHCPServerSummary struct {
+	TotalServers  int `json:"total_servers"`
+	ActiveServers int `json:"active_servers"`
+	TotalNetworks int `json:"total_networks"`
+	ActiveLeases  int `json:"active_leases"`
+	StaticLeases  int `json:"static_leases"`
+	TotalOptions  int `json:"total_options"`
+}
+
+// DHCPServerConfigStore wraps JSON persistence
+type DHCPServerConfigStore struct {
+	Servers      []DHCPServerInstance `json:"servers"`
+	Networks     []DHCPNetwork        `json:"networks"`
+	StaticLeases []DHCPLease          `json:"static_leases"`
+	Options      []DHCPOption         `json:"options"`
+}
+
+// DHCPServerSetupRequest is used by the MikroTik-style 1-click DHCP Setup Wizard
+type DHCPServerSetupRequest struct {
+	Interface    string   `json:"interface"`               // e.g. "bridge1"
+	AddressSpace string   `json:"address_space"`           // e.g. "192.168.88.0/24"
+	Gateway      string   `json:"gateway"`                 // e.g. "192.168.88.1"
+	AddressPool  string   `json:"address_pool"`            // e.g. "192.168.88.10-192.168.88.254"
+	DNSServers   []string `json:"dns_servers"`             // e.g. ["192.168.88.1", "1.1.1.1"]
+	LeaseTime    string   `json:"lease_time"`              // e.g. "12h"
+	Domain       string   `json:"domain,omitempty"`        // e.g. "lan"
+	NTPServers   []string `json:"ntp_servers,omitempty"`   // e.g. ["192.168.88.1"]
+	NextServer   string   `json:"next_server,omitempty"`   // e.g. "192.168.88.50"
+	BootFileName string   `json:"boot_file_name,omitempty"`// e.g. "pxelinux.0"
+}

+ 777 - 1
internal/web/static/app.js

@@ -44,7 +44,13 @@ let state = {
   filterChainSelected: 'all',
   activeToolsSubTab: 'ping',
   dhcpClients: [],
-  dhcpSummary: null
+  dhcpSummary: null,
+  dhcpServerTab: 'servers',
+  dhcpServers: [],
+  dhcpNetworks: [],
+  dhcpLeases: [],
+  dhcpOptions: [],
+  dhcpServerSummary: null
 };
 
 const API_BASE = '/api';
@@ -112,6 +118,7 @@ function navigate(tabName) {
     dashboard: { title: 'Dashboard Overview', sub: 'Live router performance and network telemetry' },
     interfaces: { title: 'Network Interfaces', sub: 'Interface Table View' },
     'dhcp-client': { title: 'DHCP Client', sub: 'DHCP Client List' },
+    'dhcp-server': { title: 'DHCP Server (/ip dhcp-server)', sub: 'DHCP Server instances, subnet networks (NTP, TFTP, DNS), dynamic leases & static reservations' },
     bridge: { title: 'Bridge Interfaces', sub: 'IEEE 802.1D / 802.1Q VLAN-aware Layer 2 switching, RSTP & hardware forwarding (FDB)' },
     vlans: { title: 'VLAN Virtual Networks', sub: 'IEEE 802.1Q sub-interface segmentation and routing' },
     firewall: { title: 'Network Firewall', sub: 'Stateful packet inspection, NAT, traffic mangle, address lists & connection tracking' },
@@ -171,6 +178,7 @@ function navigate(tabName) {
   if (tabName === 'pptp') loadPPTP();
   if (tabName === 'tools') loadToolsView();
   if (tabName === 'dhcp-client') loadDHCPClients();
+  if (tabName === 'dhcp-server') loadDHCPServer();
   setTimeout(initTableSorting, 50);
 }
 
@@ -250,6 +258,7 @@ async function fetchData(isManual = false) {
     if (state.activeTab === 'l2tp') loadL2TP(false);
     if (state.activeTab === 'pptp') loadPPTP(false);
     if (state.activeTab === 'dhcp-client') loadDHCPClients(false);
+    if (state.activeTab === 'dhcp-server') loadDHCPServer(false);
 
   } catch (err) {
     console.error('Failed to fetch router state:', err);
@@ -475,6 +484,10 @@ function updateBadges() {
     const bFilters = document.getElementById('badge-filters-count');
     if (bFilters) bFilters.innerText = (state.routingFilters || []).length;
   }
+  const bDhcpServer = document.getElementById('badge-dhcp-server-count');
+  if (bDhcpServer) {
+    bDhcpServer.innerText = state.dhcpServerSummary ? (state.dhcpServerSummary.active_leases || state.dhcpServerSummary.total_servers || 0) : (state.dhcpServers || []).length;
+  }
 }
 
 // Render Dashboard View
@@ -12523,6 +12536,769 @@ async function deleteDHCPClient(id, ifaceName) {
   }
 }
 
+// ==================== DHCP SERVER (/ip dhcp-server) ====================
+
+async function loadDHCPServer(populate = true) {
+  try {
+    const [sumRes, srvRes, netRes, leaseRes, optRes] = await Promise.all([
+      fetch(`${API_BASE}/dhcp-server/summary`),
+      fetch(`${API_BASE}/dhcp-server/servers`),
+      fetch(`${API_BASE}/dhcp-server/networks`),
+      fetch(`${API_BASE}/dhcp-server/leases`),
+      fetch(`${API_BASE}/dhcp-server/options`)
+    ]);
+
+    if (sumRes.ok) state.dhcpServerSummary = await sumRes.json();
+    if (srvRes.ok) state.dhcpServers = await srvRes.json();
+    if (netRes.ok) state.dhcpNetworks = await netRes.json();
+    if (leaseRes.ok) state.dhcpLeases = await leaseRes.json();
+    if (optRes.ok) state.dhcpOptions = await optRes.json();
+
+    const sum = state.dhcpServerSummary || {};
+    const srvs = state.dhcpServers || [];
+    const nets = state.dhcpNetworks || [];
+    const leases = state.dhcpLeases || [];
+    const opts = state.dhcpOptions || [];
+
+    // KPI Cards
+    const statActive = document.getElementById('dhcp-server-stat-active');
+    if (statActive) {
+      const activeCount = srvs.filter(s => s.running).length;
+      statActive.innerText = `${activeCount} / ${srvs.length}`;
+    }
+
+    const statLeases = document.getElementById('dhcp-server-stat-leases');
+    if (statLeases) {
+      statLeases.innerText = leases.length;
+    }
+
+    const statLeasesSub = document.getElementById('dhcp-server-stat-leases-sub');
+    if (statLeasesSub) {
+      const dyn = leases.filter(l => l.dynamic).length;
+      const st = leases.filter(l => !l.dynamic).length;
+      statLeasesSub.innerText = `${dyn} dynamic, ${st} static`;
+    }
+
+    const statNetworks = document.getElementById('dhcp-server-stat-networks');
+    if (statNetworks) {
+      statNetworks.innerText = nets.length;
+    }
+
+    // Subtab Badges
+    const bSrv = document.getElementById('dhcp-server-tab-badge-servers');
+    if (bSrv) bSrv.innerText = srvs.length;
+    const bNet = document.getElementById('dhcp-server-tab-badge-networks');
+    if (bNet) bNet.innerText = nets.length;
+    const bLease = document.getElementById('dhcp-server-tab-badge-leases');
+    if (bLease) bLease.innerText = leases.length;
+    const bOpt = document.getElementById('dhcp-server-tab-badge-options');
+    if (bOpt) bOpt.innerText = opts.length;
+
+    // Sidebar Badge
+    const sideBadge = document.getElementById('badge-dhcp-server-count');
+    if (sideBadge) sideBadge.innerText = leases.length || srvs.length;
+
+    renderCurrentDHCPServerTab();
+  } catch (err) {
+    console.error('Failed to load DHCP Server:', err);
+  }
+}
+
+function switchDHCPServerTab(tab) {
+  state.dhcpServerTab = tab;
+  const tabs = ['servers', 'networks', 'leases', 'options'];
+  tabs.forEach(t => {
+    const panel = document.getElementById(`dhcp-server-subtab-${t}`);
+    const btn = document.getElementById(`dhcp-server-tab-btn-${t}`);
+    if (panel) {
+      if (t === tab) panel.classList.remove('hidden');
+      else panel.classList.add('hidden');
+    }
+    if (btn) {
+      if (t === tab) {
+        btn.className = 'px-4 py-2.5 text-xs font-bold border-b-2 border-emerald-500 text-white transition flex items-center gap-2 whitespace-nowrap';
+      } else {
+        btn.className = 'px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap';
+      }
+    }
+  });
+
+  const addBtn = document.getElementById('dhcp-server-primary-add-btn');
+  if (addBtn) {
+    if (tab === 'servers') addBtn.innerHTML = '<span>+ Add Server</span>';
+    else if (tab === 'networks') addBtn.innerHTML = '<span>+ Add Network</span>';
+    else if (tab === 'leases') addBtn.innerHTML = '<span>+ Add Static Lease</span>';
+    else if (tab === 'options') addBtn.innerHTML = '<span>+ Add Option</span>';
+  }
+
+  renderCurrentDHCPServerTab();
+}
+
+function handleDHCPServerPrimaryAdd() {
+  const tab = state.dhcpServerTab || 'servers';
+  if (tab === 'servers') openAddDHCPServerModal();
+  else if (tab === 'networks') openAddDHCPNetworkModal();
+  else if (tab === 'leases') openAddDHCPStaticLeaseModal();
+  else if (tab === 'options') openAddDHCPOptionModal();
+}
+
+function renderCurrentDHCPServerTab() {
+  const tab = state.dhcpServerTab || 'servers';
+  if (tab === 'servers') renderDHCPServers();
+  else if (tab === 'networks') renderDHCPNetworks();
+  else if (tab === 'leases') renderDHCPLeases();
+  else if (tab === 'options') renderDHCPOptions();
+}
+
+function renderDHCPServers() {
+  const tbody = document.getElementById('dhcp-server-tbody');
+  const empty = document.getElementById('dhcp-server-empty');
+  if (!tbody) return;
+  tbody.innerHTML = '';
+
+  const servers = state.dhcpServers || [];
+  if (servers.length === 0) {
+    empty?.classList.remove('hidden');
+    return;
+  }
+  empty?.classList.add('hidden');
+
+  servers.forEach(s => {
+    const tr = document.createElement('tr');
+    tr.className = `hover:bg-slate-800/40 transition text-xs ${s.disabled ? 'opacity-50' : ''}`;
+    tr.innerHTML = `
+      <td class="py-2.5 px-3 text-center">${renderMikroTikFlag(s.running, s.disabled)}</td>
+      <td class="py-2.5 px-3 font-bold text-white font-mono">${escapeHtml(s.name)}</td>
+      <td class="py-2.5 px-3 font-mono text-cyan-400 font-semibold">${escapeHtml(s.interface)}</td>
+      <td class="py-2.5 px-3 font-mono text-emerald-400">${escapeHtml(s.address_pool)}</td>
+      <td class="py-2.5 px-3 font-mono text-slate-300">${escapeHtml(s.lease_time || '12h')}</td>
+      <td class="py-2.5 px-3 font-mono text-slate-300">${s.authoritative ? '<span class="text-emerald-400 font-bold">yes</span>' : '<span class="text-slate-500">no</span>'}</td>
+      <td class="py-2.5 px-3 font-mono text-cyan-300"><span class="px-1.5 py-0.5 rounded text-[10px] bg-slate-800 border border-slate-700">${s.active_leases || 0}</span></td>
+      <td class="py-2.5 px-3 text-slate-400 italic truncate max-w-[140px]" title="${escapeHtml(s.comment || '')}">${escapeHtml(s.comment || '-')}</td>
+      <td class="py-2.5 px-3 text-right space-x-1.5 font-sans whitespace-nowrap">
+        <button onclick="toggleDHCPServerState('${s.id}', ${!s.disabled})" class="text-xs ${s.disabled ? 'text-emerald-400' : 'text-slate-400'} hover:underline">${s.disabled ? 'Enable' : 'Disable'}</button>
+        <button onclick="openEditDHCPServerModal('${s.id}')" class="text-xs text-slate-400 hover:text-white">Edit</button>
+        <button onclick="deleteDHCPServer('${s.id}', '${escapeHtml(s.name)}')" class="text-xs text-rose-400 hover:text-rose-300">Delete</button>
+      </td>
+    `;
+    tbody.appendChild(tr);
+  });
+}
+
+function renderDHCPNetworks() {
+  const tbody = document.getElementById('dhcp-networks-tbody');
+  const empty = document.getElementById('dhcp-networks-empty');
+  if (!tbody) return;
+  tbody.innerHTML = '';
+
+  const networks = state.dhcpNetworks || [];
+  if (networks.length === 0) {
+    empty?.classList.remove('hidden');
+    return;
+  }
+  empty?.classList.add('hidden');
+
+  networks.forEach(n => {
+    const tr = document.createElement('tr');
+    tr.className = 'hover:bg-slate-800/40 transition text-xs';
+
+    const dnsStr = (n.dns_servers || []).join(', ') || '-';
+    const ntpStr = (n.ntp_servers || []).join(', ') || '-';
+
+    tr.innerHTML = `
+      <td class="py-2.5 px-3 font-bold text-emerald-400 font-mono">${escapeHtml(n.address)}</td>
+      <td class="py-2.5 px-3 font-mono text-cyan-300 font-semibold">${escapeHtml(n.gateway || '-')}</td>
+      <td class="py-2.5 px-3 font-mono text-slate-300">${escapeHtml(n.netmask || '-')}</td>
+      <td class="py-2.5 px-3 font-mono text-slate-300 truncate max-w-[120px]" title="${escapeHtml(dnsStr)}">${escapeHtml(dnsStr)}</td>
+      <td class="py-2.5 px-3 font-mono text-slate-400">${escapeHtml(n.domain || '-')}</td>
+      <td class="py-2.5 px-3 font-mono text-purple-300 truncate max-w-[120px]" title="${escapeHtml(ntpStr)}">${escapeHtml(ntpStr)}</td>
+      <td class="py-2.5 px-3 font-mono text-amber-300">${escapeHtml(n.next_server || '-')}</td>
+      <td class="py-2.5 px-3 font-mono text-slate-300">${escapeHtml(n.boot_file_name || '-')}</td>
+      <td class="py-2.5 px-3 text-slate-400 italic truncate max-w-[120px]" title="${escapeHtml(n.comment || '')}">${escapeHtml(n.comment || '-')}</td>
+      <td class="py-2.5 px-3 text-right space-x-1.5 font-sans whitespace-nowrap">
+        <button onclick="openEditDHCPNetworkModal('${n.id}')" class="text-xs text-slate-400 hover:text-white">Edit</button>
+        <button onclick="deleteDHCPNetwork('${n.id}', '${escapeHtml(n.address)}')" class="text-xs text-rose-400 hover:text-rose-300">Delete</button>
+      </td>
+    `;
+    tbody.appendChild(tr);
+  });
+}
+
+function renderDHCPLeases() {
+  const tbody = document.getElementById('dhcp-leases-tbody');
+  const empty = document.getElementById('dhcp-leases-empty');
+  if (!tbody) return;
+  tbody.innerHTML = '';
+
+  const leases = state.dhcpLeases || [];
+  if (leases.length === 0) {
+    empty?.classList.remove('hidden');
+    return;
+  }
+  empty?.classList.add('hidden');
+
+  leases.forEach(l => {
+    const tr = document.createElement('tr');
+    tr.className = `hover:bg-slate-800/40 transition text-xs ${l.disabled ? 'opacity-50' : ''}`;
+
+    const flag = l.dynamic ? '<span class="text-sky-400 font-bold" title="Dynamic">D</span>' : '<span class="text-purple-400 font-bold" title="Static Reservation">S</span>';
+    const statusBadge = l.status === 'bound'
+      ? '<span class="px-1.5 py-0.5 rounded text-[10px] bg-emerald-500/20 text-emerald-300 border border-emerald-500/30 font-semibold font-mono">bound</span>'
+      : l.status === 'static'
+      ? '<span class="px-1.5 py-0.5 rounded text-[10px] bg-purple-500/20 text-purple-300 border border-purple-500/30 font-semibold font-mono">static</span>'
+      : '<span class="px-1.5 py-0.5 rounded text-[10px] bg-slate-800 text-slate-400 border border-slate-700 font-mono">waiting</span>';
+
+    const makeStaticBtn = l.dynamic
+      ? `<button onclick="makeDHCPLeaseStatic('${escapeHtml(l.mac_address)}', '${escapeHtml(l.address)}', '${escapeHtml(l.hostname || '')}', '${escapeHtml(l.comment || '')}')" class="text-xs px-2 py-0.5 rounded bg-purple-500/20 text-purple-300 border border-purple-500/30 hover:bg-purple-500/30 font-medium">Make Static</button>`
+      : `<button onclick="deleteDHCPStaticLease('${l.id}')" class="text-xs text-rose-400 hover:text-rose-300 font-medium">Remove</button>`;
+
+    tr.innerHTML = `
+      <td class="py-2.5 px-3 text-center">${flag}</td>
+      <td class="py-2.5 px-3 font-bold text-white font-mono">${escapeHtml(l.address)}</td>
+      <td class="py-2.5 px-3 font-mono text-cyan-300">${escapeHtml(l.mac_address)}</td>
+      <td class="py-2.5 px-3 font-sans text-slate-200 font-medium">${escapeHtml(l.hostname || '*')}</td>
+      <td class="py-2.5 px-3 font-mono text-slate-400">${escapeHtml(l.server || 'all')}</td>
+      <td class="py-2.5 px-3 font-mono text-slate-300">${escapeHtml(l.expires_in || 'never')}</td>
+      <td class="py-2.5 px-3">${statusBadge}</td>
+      <td class="py-2.5 px-3 text-slate-400 italic truncate max-w-[120px]" title="${escapeHtml(l.comment || '')}">${escapeHtml(l.comment || '-')}</td>
+      <td class="py-2.5 px-3 text-right space-x-1.5 font-sans whitespace-nowrap">
+        ${makeStaticBtn}
+      </td>
+    `;
+    tbody.appendChild(tr);
+  });
+}
+
+function renderDHCPOptions() {
+  const tbody = document.getElementById('dhcp-options-tbody');
+  const empty = document.getElementById('dhcp-options-empty');
+  if (!tbody) return;
+  tbody.innerHTML = '';
+
+  const options = state.dhcpOptions || [];
+  if (options.length === 0) {
+    empty?.classList.remove('hidden');
+    return;
+  }
+  empty?.classList.add('hidden');
+
+  options.forEach(o => {
+    const tr = document.createElement('tr');
+    tr.className = 'hover:bg-slate-800/40 transition text-xs';
+    tr.innerHTML = `
+      <td class="py-2.5 px-3 font-bold text-white font-mono">${escapeHtml(o.name)}</td>
+      <td class="py-2.5 px-3 font-bold text-cyan-400 font-mono">${o.code}</td>
+      <td class="py-2.5 px-3 font-mono text-emerald-400 break-all max-w-xs">${escapeHtml(o.value)}</td>
+      <td class="py-2.5 px-3 text-slate-400 italic truncate max-w-xs" title="${escapeHtml(o.comment || '')}">${escapeHtml(o.comment || '-')}</td>
+      <td class="py-2.5 px-3 text-right space-x-1.5 font-sans whitespace-nowrap">
+        <button onclick="openEditDHCPOptionModal('${o.id}')" class="text-xs text-slate-400 hover:text-white">Edit</button>
+        <button onclick="deleteDHCPOption('${o.id}', '${escapeHtml(o.name)}')" class="text-xs text-rose-400 hover:text-rose-300">Delete</button>
+      </td>
+    `;
+    tbody.appendChild(tr);
+  });
+}
+
+// ==================== MODALS & ACTIONS: SERVER ====================
+function openAddDHCPServerModal() {
+  document.getElementById('dhcp-server-modal-title').innerText = 'Add DHCP Server';
+  document.getElementById('dhcp-server-modal-id').value = '';
+  document.getElementById('dhcp-server-modal-name').value = `dhcp${(state.dhcpServers || []).length + 1}`;
+  document.getElementById('dhcp-server-modal-pool').value = '192.168.88.10-192.168.88.254';
+  document.getElementById('dhcp-server-modal-leasetime').value = '12h';
+  document.getElementById('dhcp-server-modal-comment').value = '';
+  document.getElementById('dhcp-server-modal-authoritative').checked = true;
+  document.getElementById('dhcp-server-modal-disabled').checked = false;
+
+  populateDHCPIfaceSelect('dhcp-server-modal-iface', '');
+  document.getElementById('dhcp-server-instance-modal').classList.remove('hidden');
+}
+
+function openEditDHCPServerModal(id) {
+  const s = (state.dhcpServers || []).find(item => item.id === id);
+  if (!s) return;
+  document.getElementById('dhcp-server-modal-title').innerText = `Edit DHCP Server (${s.name})`;
+  document.getElementById('dhcp-server-modal-id').value = s.id;
+  document.getElementById('dhcp-server-modal-name').value = s.name;
+  document.getElementById('dhcp-server-modal-pool').value = s.address_pool;
+  document.getElementById('dhcp-server-modal-leasetime').value = s.lease_time || '12h';
+  document.getElementById('dhcp-server-modal-comment').value = s.comment || '';
+  document.getElementById('dhcp-server-modal-authoritative').checked = s.authoritative !== false;
+  document.getElementById('dhcp-server-modal-disabled').checked = !!s.disabled;
+
+  populateDHCPIfaceSelect('dhcp-server-modal-iface', s.interface);
+  document.getElementById('dhcp-server-instance-modal').classList.remove('hidden');
+}
+
+function closeDHCPServerModal() {
+  document.getElementById('dhcp-server-instance-modal').classList.add('hidden');
+}
+
+async function submitSaveDHCPServer(e) {
+  e.preventDefault();
+  const id = document.getElementById('dhcp-server-modal-id').value;
+  const name = document.getElementById('dhcp-server-modal-name').value.trim();
+  const iface = document.getElementById('dhcp-server-modal-iface').value;
+  const pool = document.getElementById('dhcp-server-modal-pool').value.trim();
+  const leaseTime = document.getElementById('dhcp-server-modal-leasetime').value.trim() || '12h';
+  const comment = document.getElementById('dhcp-server-modal-comment').value.trim();
+  const authoritative = document.getElementById('dhcp-server-modal-authoritative').checked;
+  const disabled = document.getElementById('dhcp-server-modal-disabled').checked;
+
+  try {
+    const res = await fetch(`${API_BASE}/dhcp-server/servers`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        id,
+        name,
+        interface: iface,
+        address_pool: pool,
+        lease_time: leaseTime,
+        authoritative,
+        disabled,
+        comment
+      })
+    });
+    if (res.ok) {
+      showToast('DHCP Server Saved', `Server ${name} configured successfully`, 'success');
+      closeDHCPServerModal();
+      loadDHCPServer(false);
+      fetchData(false);
+    } else {
+      const err = await res.text();
+      showToast('Error', err, 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function toggleDHCPServerState(id, disabled) {
+  try {
+    const res = await fetch(`${API_BASE}/dhcp-server/servers/${encodeURIComponent(id)}/state`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ disabled })
+    });
+    if (res.ok) {
+      showToast('State Updated', disabled ? 'Server disabled' : 'Server enabled', 'info');
+      loadDHCPServer(false);
+      fetchData(false);
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function deleteDHCPServer(id, name) {
+  if (!confirm(`Are you sure you want to delete DHCP Server "${name}"?`)) return;
+  try {
+    const res = await fetch(`${API_BASE}/dhcp-server/servers/${encodeURIComponent(id)}`, { method: 'DELETE' });
+    if (res.ok) {
+      showToast('Deleted', `DHCP Server ${name} removed`, 'success');
+      loadDHCPServer(false);
+      fetchData(false);
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+// ==================== MODALS & ACTIONS: NETWORKS ====================
+function openAddDHCPNetworkModal() {
+  document.getElementById('dhcp-network-modal-title').innerText = 'Add DHCP Network';
+  document.getElementById('dhcp-network-modal-id').value = '';
+  document.getElementById('dhcp-network-modal-address').value = '192.168.88.0/24';
+  document.getElementById('dhcp-network-modal-gateway').value = '192.168.88.1';
+  document.getElementById('dhcp-network-modal-dns').value = '192.168.88.1, 1.1.1.1, 8.8.8.8';
+  document.getElementById('dhcp-network-modal-domain').value = 'lan';
+  document.getElementById('dhcp-network-modal-ntp').value = '192.168.88.1, time.cloudflare.com';
+  document.getElementById('dhcp-network-modal-tftp').value = '';
+  document.getElementById('dhcp-network-modal-bootfile').value = '';
+  document.getElementById('dhcp-network-modal-wins').value = '';
+  document.getElementById('dhcp-network-modal-netmask').value = '';
+  document.getElementById('dhcp-network-modal-comment').value = '';
+
+  renderDHCPNetworkOptionsCheckboxes([]);
+  document.getElementById('dhcp-network-modal').classList.remove('hidden');
+}
+
+function openEditDHCPNetworkModal(id) {
+  const n = (state.dhcpNetworks || []).find(item => item.id === id);
+  if (!n) return;
+
+  document.getElementById('dhcp-network-modal-title').innerText = `Edit DHCP Network (${n.address})`;
+  document.getElementById('dhcp-network-modal-id').value = n.id;
+  document.getElementById('dhcp-network-modal-address').value = n.address;
+  document.getElementById('dhcp-network-modal-gateway').value = n.gateway || '';
+  document.getElementById('dhcp-network-modal-dns').value = (n.dns_servers || []).join(', ');
+  document.getElementById('dhcp-network-modal-domain').value = n.domain || '';
+  document.getElementById('dhcp-network-modal-ntp').value = (n.ntp_servers || []).join(', ');
+  document.getElementById('dhcp-network-modal-tftp').value = n.next_server || '';
+  document.getElementById('dhcp-network-modal-bootfile').value = n.boot_file_name || '';
+  document.getElementById('dhcp-network-modal-wins').value = (n.wins_servers || []).join(', ');
+  document.getElementById('dhcp-network-modal-netmask').value = n.netmask || '';
+  document.getElementById('dhcp-network-modal-comment').value = n.comment || '';
+
+  renderDHCPNetworkOptionsCheckboxes(n.dhcp_options || []);
+  document.getElementById('dhcp-network-modal').classList.remove('hidden');
+}
+
+function renderDHCPNetworkOptionsCheckboxes(selectedIds) {
+  const container = document.getElementById('dhcp-network-modal-options-list');
+  if (!container) return;
+  container.innerHTML = '';
+
+  const opts = state.dhcpOptions || [];
+  if (opts.length === 0) {
+    container.innerHTML = '<span class="text-slate-500 text-[11px] col-span-2">No custom options configured. Add some in the "Options" tab.</span>';
+    return;
+  }
+
+  opts.forEach(o => {
+    const isChecked = selectedIds.includes(o.id);
+    const label = document.createElement('label');
+    label.className = 'flex items-center space-x-2 text-slate-300 hover:text-white cursor-pointer';
+    label.innerHTML = `
+      <input type="checkbox" name="dhcp-net-opt" value="${o.id}" ${isChecked ? 'checked' : ''} class="rounded bg-slate-800 border-slate-700 text-emerald-600">
+      <span class="truncate" title="${escapeHtml(o.name)} (Code ${o.code})">${escapeHtml(o.name)} <span class="text-cyan-400">#${o.code}</span></span>
+    `;
+    container.appendChild(label);
+  });
+}
+
+function closeDHCPNetworkModal() {
+  document.getElementById('dhcp-network-modal').classList.add('hidden');
+}
+
+async function submitSaveDHCPNetwork(e) {
+  e.preventDefault();
+  const id = document.getElementById('dhcp-network-modal-id').value;
+  const address = document.getElementById('dhcp-network-modal-address').value.trim();
+  const gateway = document.getElementById('dhcp-network-modal-gateway').value.trim();
+  const dnsStr = document.getElementById('dhcp-network-modal-dns').value.trim();
+  const domain = document.getElementById('dhcp-network-modal-domain').value.trim();
+  const ntpStr = document.getElementById('dhcp-network-modal-ntp').value.trim();
+  const tftp = document.getElementById('dhcp-network-modal-tftp').value.trim();
+  const bootfile = document.getElementById('dhcp-network-modal-bootfile').value.trim();
+  const winsStr = document.getElementById('dhcp-network-modal-wins').value.trim();
+  const netmask = document.getElementById('dhcp-network-modal-netmask').value.trim();
+  const comment = document.getElementById('dhcp-network-modal-comment').value.trim();
+
+  const selectedOpts = [];
+  document.querySelectorAll('input[name="dhcp-net-opt"]:checked').forEach(cb => selectedOpts.push(cb.value));
+
+  const dnsServers = dnsStr ? dnsStr.split(',').map(s => s.trim()).filter(Boolean) : [];
+  const ntpServers = ntpStr ? ntpStr.split(',').map(s => s.trim()).filter(Boolean) : [];
+  const winsServers = winsStr ? winsStr.split(',').map(s => s.trim()).filter(Boolean) : [];
+
+  try {
+    const res = await fetch(`${API_BASE}/dhcp-server/networks`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        id,
+        address,
+        gateway,
+        netmask,
+        dns_servers: dnsServers,
+        domain,
+        ntp_servers: ntpServers,
+        next_server: tftp,
+        boot_file_name: bootfile,
+        wins_servers: winsServers,
+        dhcp_options: selectedOpts,
+        comment
+      })
+    });
+    if (res.ok) {
+      showToast('Network Saved', `Subnet ${address} configured successfully`, 'success');
+      closeDHCPNetworkModal();
+      loadDHCPServer(false);
+      fetchData(false);
+    } else {
+      const err = await res.text();
+      showToast('Error', err, 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function deleteDHCPNetwork(id, addr) {
+  if (!confirm(`Are you sure you want to delete DHCP Network "${addr}"?`)) return;
+  try {
+    const res = await fetch(`${API_BASE}/dhcp-server/networks/${encodeURIComponent(id)}`, { method: 'DELETE' });
+    if (res.ok) {
+      showToast('Deleted', `Network ${addr} removed`, 'success');
+      loadDHCPServer(false);
+      fetchData(false);
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+// ==================== MODALS & ACTIONS: LEASES ====================
+function openAddDHCPStaticLeaseModal() {
+  document.getElementById('dhcp-static-lease-modal-title').innerText = 'Add Static Lease Reservation';
+  document.getElementById('dhcp-static-lease-modal-id').value = '';
+  document.getElementById('dhcp-static-lease-modal-mac').value = '';
+  document.getElementById('dhcp-static-lease-modal-ip').value = '';
+  document.getElementById('dhcp-static-lease-modal-hostname').value = '';
+  document.getElementById('dhcp-static-lease-modal-server').value = 'all';
+  document.getElementById('dhcp-static-lease-modal-comment').value = '';
+  document.getElementById('dhcp-static-lease-modal-disabled').checked = false;
+  document.getElementById('dhcp-static-lease-modal').classList.remove('hidden');
+}
+
+function closeDHCPStaticLeaseModal() {
+  document.getElementById('dhcp-static-lease-modal').classList.add('hidden');
+}
+
+async function submitSaveDHCPStaticLease(e) {
+  e.preventDefault();
+  const id = document.getElementById('dhcp-static-lease-modal-id').value;
+  const mac = document.getElementById('dhcp-static-lease-modal-mac').value.trim();
+  const ip = document.getElementById('dhcp-static-lease-modal-ip').value.trim();
+  const hostname = document.getElementById('dhcp-static-lease-modal-hostname').value.trim();
+  const server = document.getElementById('dhcp-static-lease-modal-server').value.trim() || 'all';
+  const comment = document.getElementById('dhcp-static-lease-modal-comment').value.trim();
+  const disabled = document.getElementById('dhcp-static-lease-modal-disabled').checked;
+
+  try {
+    const res = await fetch(`${API_BASE}/dhcp-server/leases/static`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        id,
+        mac_address: mac,
+        address: ip,
+        hostname,
+        server,
+        comment,
+        disabled
+      })
+    });
+    if (res.ok) {
+      showToast('Static Lease Saved', `IP ${ip} reserved for ${mac}`, 'success');
+      closeDHCPStaticLeaseModal();
+      loadDHCPServer(false);
+      fetchData(false);
+    } else {
+      const err = await res.text();
+      showToast('Error', err, 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function deleteDHCPStaticLease(id) {
+  if (!confirm('Are you sure you want to remove this static reservation?')) return;
+  try {
+    const res = await fetch(`${API_BASE}/dhcp-server/leases/${encodeURIComponent(id)}`, { method: 'DELETE' });
+    if (res.ok) {
+      showToast('Deleted', 'Static lease removed', 'success');
+      loadDHCPServer(false);
+      fetchData(false);
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function makeDHCPLeaseStatic(mac, ip, hostname, comment) {
+  try {
+    const res = await fetch(`${API_BASE}/dhcp-server/leases/make-static`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        mac_address: mac,
+        address: ip,
+        hostname,
+        comment: comment || 'Converted to static'
+      })
+    });
+    if (res.ok) {
+      showToast('Lease Made Static', `IP ${ip} permanently reserved for ${mac}`, 'success');
+      loadDHCPServer(false);
+      fetchData(false);
+    } else {
+      const err = await res.text();
+      showToast('Error', err, 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+// ==================== MODALS & ACTIONS: OPTIONS ====================
+function openAddDHCPOptionModal() {
+  document.getElementById('dhcp-option-modal-title').innerText = 'Add DHCP Option';
+  document.getElementById('dhcp-option-modal-id').value = '';
+  document.getElementById('dhcp-option-modal-name').value = '';
+  document.getElementById('dhcp-option-modal-code').value = '';
+  document.getElementById('dhcp-option-modal-value').value = '';
+  document.getElementById('dhcp-option-modal-comment').value = '';
+  document.getElementById('dhcp-option-modal').classList.remove('hidden');
+}
+
+function openEditDHCPOptionModal(id) {
+  const o = (state.dhcpOptions || []).find(item => item.id === id);
+  if (!o) return;
+  document.getElementById('dhcp-option-modal-title').innerText = `Edit DHCP Option (${o.name})`;
+  document.getElementById('dhcp-option-modal-id').value = o.id;
+  document.getElementById('dhcp-option-modal-name').value = o.name;
+  document.getElementById('dhcp-option-modal-code').value = o.code;
+  document.getElementById('dhcp-option-modal-value').value = o.value;
+  document.getElementById('dhcp-option-modal-comment').value = o.comment || '';
+  document.getElementById('dhcp-option-modal').classList.remove('hidden');
+}
+
+function closeDHCPOptionModal() {
+  document.getElementById('dhcp-option-modal').classList.add('hidden');
+}
+
+async function submitSaveDHCPOption(e) {
+  e.preventDefault();
+  const id = document.getElementById('dhcp-option-modal-id').value;
+  const name = document.getElementById('dhcp-option-modal-name').value.trim();
+  const code = parseInt(document.getElementById('dhcp-option-modal-code').value);
+  const value = document.getElementById('dhcp-option-modal-value').value.trim();
+  const comment = document.getElementById('dhcp-option-modal-comment').value.trim();
+
+  try {
+    const res = await fetch(`${API_BASE}/dhcp-server/options`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({ id, name, code, value, comment })
+    });
+    if (res.ok) {
+      showToast('Option Saved', `DHCP option ${name} (#${code}) configured`, 'success');
+      closeDHCPOptionModal();
+      loadDHCPServer(false);
+      fetchData(false);
+    } else {
+      const err = await res.text();
+      showToast('Error', err, 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+async function deleteDHCPOption(id, name) {
+  if (!confirm(`Are you sure you want to delete DHCP Option "${name}"?`)) return;
+  try {
+    const res = await fetch(`${API_BASE}/dhcp-server/options/${encodeURIComponent(id)}`, { method: 'DELETE' });
+    if (res.ok) {
+      showToast('Deleted', `Option ${name} removed`, 'success');
+      loadDHCPServer(false);
+      fetchData(false);
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+// ==================== WIZARD: DHCP SETUP (MikroTik style) ====================
+function openDHCPServerSetupModal() {
+  populateDHCPIfaceSelect('dhcp-wizard-iface', '');
+  onDHCPWizardIfaceChange();
+  document.getElementById('dhcp-setup-wizard-modal').classList.remove('hidden');
+}
+
+function closeDHCPServerSetupModal() {
+  document.getElementById('dhcp-setup-wizard-modal').classList.add('hidden');
+}
+
+function onDHCPWizardIfaceChange() {
+  const ifaceName = document.getElementById('dhcp-wizard-iface').value;
+  const iface = (state.interfaces || []).find(i => i.name === ifaceName);
+
+  let ip = '192.168.88.1';
+  let prefix = '24';
+  if (iface && iface.ip_address) {
+    const parts = iface.ip_address.split('/');
+    ip = parts[0];
+    if (parts.length > 1) prefix = parts[1];
+  }
+
+  // Calculate subnet base (e.g. 192.168.88.0/24)
+  const ipParts = ip.split('.');
+  if (ipParts.length === 4) {
+    const baseSpace = `${ipParts[0]}.${ipParts[1]}.${ipParts[2]}.0/${prefix}`;
+    const pool = `${ipParts[0]}.${ipParts[1]}.${ipParts[2]}.10-${ipParts[0]}.${ipParts[1]}.${ipParts[2]}.254`;
+    document.getElementById('dhcp-wizard-space').value = baseSpace;
+    document.getElementById('dhcp-wizard-gateway').value = ip;
+    document.getElementById('dhcp-wizard-pool').value = pool;
+    document.getElementById('dhcp-wizard-dns').value = `${ip}, 1.1.1.1, 8.8.8.8`;
+    document.getElementById('dhcp-wizard-ntp').value = `${ip}`;
+  }
+}
+
+async function submitDHCPServerSetupWizard(e) {
+  e.preventDefault();
+  const iface = document.getElementById('dhcp-wizard-iface').value;
+  const space = document.getElementById('dhcp-wizard-space').value.trim();
+  const gw = document.getElementById('dhcp-wizard-gateway').value.trim();
+  const pool = document.getElementById('dhcp-wizard-pool').value.trim();
+  const dnsStr = document.getElementById('dhcp-wizard-dns').value.trim();
+  const leaseTime = document.getElementById('dhcp-wizard-leasetime').value.trim() || '12h';
+  const ntpStr = document.getElementById('dhcp-wizard-ntp').value.trim();
+  const tftp = document.getElementById('dhcp-wizard-tftp').value.trim();
+
+  const dnsServers = dnsStr ? dnsStr.split(',').map(s => s.trim()).filter(Boolean) : [];
+  const ntpServers = ntpStr ? ntpStr.split(',').map(s => s.trim()).filter(Boolean) : [];
+
+  try {
+    const res = await fetch(`${API_BASE}/dhcp-server/setup`, {
+      method: 'POST',
+      headers: { 'Content-Type': 'application/json' },
+      body: JSON.stringify({
+        interface: iface,
+        address_space: space,
+        gateway: gw,
+        address_pool: pool,
+        dns_servers: dnsServers,
+        lease_time: leaseTime,
+        ntp_servers: ntpServers,
+        next_server: tftp
+      })
+    });
+    if (res.ok) {
+      showToast('DHCP Setup Complete', `DHCP Server setup finished on ${iface}`, 'success');
+      closeDHCPServerSetupModal();
+      loadDHCPServer(false);
+      fetchData(false);
+    } else {
+      const err = await res.text();
+      showToast('Error', err, 'error');
+    }
+  } catch (err) {
+    showToast('Error', err.message, 'error');
+  }
+}
+
+function populateDHCPIfaceSelect(elemId, selectedVal) {
+  const sel = document.getElementById(elemId);
+  if (!sel) return;
+  sel.innerHTML = '<option value="">Select interface...</option>';
+
+  const ifaces = state.interfaces || [];
+  ifaces.forEach(i => {
+    if (i.name === 'lo') return;
+    const opt = document.createElement('option');
+    opt.value = i.name;
+    const ipStr = i.ip_address ? ` (${i.ip_address})` : '';
+    opt.textContent = `${i.name}${ipStr} [${i.role || i.type || 'interface'}]`;
+    if (i.name === selectedVal) opt.selected = true;
+    sel.appendChild(opt);
+  });
+}
+
 // ==================== UNIVERSAL TABLE ORDERING / SORTING (WINBOX / WEBFIG STYLE) ====================
 
 function initTableSorting() {

+ 582 - 1
internal/web/static/index.html

@@ -110,6 +110,17 @@
           <span id="badge-dhcp-client-count" class="text-[10px] bg-slate-800 text-slate-300 px-1.5 py-0.2 rounded-full font-mono">0</span>
         </a>
 
+        <a href="#dhcp-server" onclick="navigate('dhcp-server')" id="nav-dhcp-server"
+           class="nav-link flex items-center justify-between px-3 py-2 rounded-lg text-xs font-medium transition-all group text-slate-300 hover:bg-slate-800/80 hover:text-white">
+          <div class="flex items-center space-x-2.5">
+            <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="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01" />
+            </svg>
+            <span>DHCP Server</span>
+          </div>
+          <span id="badge-dhcp-server-count" class="text-[10px] bg-emerald-950 text-emerald-300 border border-emerald-800/40 px-1.5 py-0.2 rounded-full font-mono">0</span>
+        </a>
+
         <a href="#bridge" onclick="navigate('bridge')" id="nav-bridge"
            class="nav-link flex items-center justify-between px-3 py-2 rounded-lg text-xs font-medium transition-all group text-slate-300 hover:bg-slate-800/80 hover:text-white">
           <div class="flex items-center space-x-2.5">
@@ -648,6 +659,246 @@
           </div>
         </section>
 
+        <!-- ================= SECTION: DHCP SERVER (/ip dhcp-server) ================= -->
+        <section id="section-dhcp-server" class="hidden space-y-6">
+          <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">/ip dhcp-server</span>
+                <h2 class="text-base font-bold text-white flex items-center gap-2">
+                  <span>DHCP Server & Subnets</span>
+                  <span class="px-2 py-0.5 text-[10px] rounded-full bg-emerald-500/20 text-emerald-300 border border-emerald-500/40 font-mono font-normal">MikroTik RouterOS Architecture</span>
+                </h2>
+              </div>
+              <p class="text-xs text-slate-400">Dynamic Host Configuration Protocol server, subnet network parameters (NTP, TFTP/PXE, DNS, Option Codes), active lease allocations & static IP reservations.</p>
+            </div>
+            <div class="flex items-center gap-2">
+              <button onclick="openDHCPServerSetupModal()" class="text-xs font-semibold px-3 py-2 rounded-lg bg-emerald-600/30 hover:bg-emerald-600/50 text-emerald-300 border border-emerald-500/40 transition flex items-center gap-1.5" title="Run MikroTik 1-Click DHCP Setup Wizard">
+                <span>⚡ DHCP Setup</span>
+              </button>
+              <button id="dhcp-server-primary-add-btn" onclick="handleDHCPServerPrimaryAdd()" class="text-xs font-semibold px-4 py-2 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white shadow-lg shadow-emerald-500/20 transition flex items-center gap-1.5">
+                <span>+ Add Server</span>
+              </button>
+              <button onclick="loadDHCPServer(true)" class="text-xs font-semibold px-3 py-2 rounded-lg bg-slate-800 hover:bg-slate-700 text-slate-300 border border-slate-700 transition" title="Refresh">
+                <span>🔄</span>
+              </button>
+            </div>
+          </div>
+
+          <!-- MikroTik Subtabs -->
+          <div class="flex items-center border-b border-slate-800 overflow-x-auto space-x-1">
+            <button id="dhcp-server-tab-btn-servers" onclick="switchDHCPServerTab('servers')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-emerald-500 text-white transition flex items-center gap-2 whitespace-nowrap">
+              <span>DHCP</span>
+              <span id="dhcp-server-tab-badge-servers" class="px-1.5 py-0.2 rounded-full text-[10px] bg-slate-800 text-slate-300 font-mono">0</span>
+            </button>
+            <button id="dhcp-server-tab-btn-networks" onclick="switchDHCPServerTab('networks')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
+              <span>Networks</span>
+              <span id="dhcp-server-tab-badge-networks" class="px-1.5 py-0.2 rounded-full text-[10px] bg-slate-800 text-slate-300 font-mono">0</span>
+            </button>
+            <button id="dhcp-server-tab-btn-leases" onclick="switchDHCPServerTab('leases')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
+              <span>Leases</span>
+              <span id="dhcp-server-tab-badge-leases" class="px-1.5 py-0.2 rounded-full text-[10px] bg-slate-800 text-slate-300 font-mono">0</span>
+            </button>
+            <button id="dhcp-server-tab-btn-options" onclick="switchDHCPServerTab('options')" class="px-4 py-2.5 text-xs font-bold border-b-2 border-transparent text-slate-400 hover:text-slate-200 transition flex items-center gap-2 whitespace-nowrap">
+              <span>Options</span>
+              <span id="dhcp-server-tab-badge-options" class="px-1.5 py-0.2 rounded-full text-[10px] bg-slate-800 text-slate-300 font-mono">0</span>
+            </button>
+          </div>
+
+          <!-- KPI Summary Cards -->
+          <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
+            <div class="bg-slate-900 border border-slate-800 rounded-xl p-4 flex items-center justify-between">
+              <div>
+                <div class="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">Active Servers</div>
+                <div id="dhcp-server-stat-active" class="text-xl font-bold text-white font-mono mt-1">0 / 0</div>
+                <div class="text-[10px] text-slate-500 mt-0.5">Instances online</div>
+              </div>
+              <div class="w-10 h-10 rounded-xl bg-emerald-500/10 border border-emerald-500/20 flex items-center justify-center text-emerald-400">
+                <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M5 12a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v4a2 2 0 01-2 2M5 12a2 2 0 00-2 2v4a2 2 0 002 2h14a2 2 0 002-2v-4a2 2 0 00-2-2m-2-4h.01M17 16h.01"/></svg>
+              </div>
+            </div>
+
+            <div class="bg-slate-900 border border-slate-800 rounded-xl p-4 flex items-center justify-between">
+              <div>
+                <div class="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">Active Leases</div>
+                <div id="dhcp-server-stat-leases" class="text-xl font-bold text-emerald-400 font-mono mt-1">0</div>
+                <div id="dhcp-server-stat-leases-sub" class="text-[10px] text-slate-500 mt-0.5">0 dynamic, 0 static</div>
+              </div>
+              <div class="w-10 h-10 rounded-xl bg-sky-500/10 border border-sky-500/20 flex items-center justify-center text-sky-400">
+                <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>
+              </div>
+            </div>
+
+            <div class="bg-slate-900 border border-slate-800 rounded-xl p-4 flex items-center justify-between">
+              <div>
+                <div class="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">Configured Subnets</div>
+                <div id="dhcp-server-stat-networks" class="text-xl font-bold text-cyan-400 font-mono mt-1">0</div>
+                <div class="text-[10px] text-slate-500 mt-0.5">DHCP network scopes</div>
+              </div>
+              <div class="w-10 h-10 rounded-xl bg-cyan-500/10 border border-cyan-500/20 flex items-center justify-center text-cyan-400">
+                <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16"/></svg>
+              </div>
+            </div>
+
+            <div class="bg-slate-900 border border-slate-800 rounded-xl p-4 flex items-center justify-between">
+              <div>
+                <div class="text-[11px] font-semibold text-slate-400 uppercase tracking-wider">DHCP Engine</div>
+                <div class="text-xs font-bold text-emerald-300 font-mono mt-1">dnsmasq v2.91</div>
+                <div class="text-[10px] text-slate-400 mt-0.5">Authoritative Mode • Port 67</div>
+              </div>
+              <div class="w-10 h-10 rounded-xl bg-indigo-500/10 border border-indigo-500/20 flex items-center justify-center text-indigo-400">
+                <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"/></svg>
+              </div>
+            </div>
+          </div>
+
+          <!-- SUBTAB 1: SERVERS (/ip dhcp-server) -->
+          <div id="dhcp-server-subtab-servers" class="space-y-4">
+            <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>
+                  <h3 class="text-xs font-bold text-white">DHCP Server Instances</h3>
+                  <p class="text-[11px] text-slate-400 mt-0.5">Per-interface DHCP daemon listeners and address pools</p>
+                </div>
+                <div class="text-xs text-slate-400 font-mono">
+                  <span class="text-emerald-400">R</span> = Running &nbsp;|&nbsp; <span class="text-rose-400">X</span> = Disabled
+                </div>
+              </div>
+              <div class="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">
+                    <tr>
+                      <th class="py-2.5 px-3 w-12 text-center">Flag</th>
+                      <th class="py-2.5 px-3">Name</th>
+                      <th class="py-2.5 px-3">Interface</th>
+                      <th class="py-2.5 px-3">Address Pool</th>
+                      <th class="py-2.5 px-3">Lease Time</th>
+                      <th class="py-2.5 px-3">Authoritative</th>
+                      <th class="py-2.5 px-3">Active Leases</th>
+                      <th class="py-2.5 px-3">Comment</th>
+                      <th class="py-2.5 px-3 text-right font-sans">Actions</th>
+                    </tr>
+                  </thead>
+                  <tbody id="dhcp-server-tbody" class="divide-y divide-slate-800/80"></tbody>
+                </table>
+              </div>
+              <div id="dhcp-server-empty" class="hidden p-10 text-center text-slate-500 text-xs font-sans">
+                No DHCP servers configured. Click "+ Add Server" or "⚡ DHCP Setup" to configure one.
+              </div>
+            </div>
+          </div>
+
+          <!-- SUBTAB 2: NETWORKS (/ip dhcp-server network) -->
+          <div id="dhcp-server-subtab-networks" class="hidden space-y-4">
+            <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>
+                  <h3 class="text-xs font-bold text-white">DHCP Subnet Networks (/ip dhcp-server network)</h3>
+                  <p class="text-[11px] text-slate-400 mt-0.5">Subnet parameters, Gateways, DNS, NTP servers, and TFTP / PXE Boot options</p>
+                </div>
+                <button onclick="openAddDHCPNetworkModal()" class="text-xs font-semibold px-3 py-1.5 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white transition">
+                  + Add Network
+                </button>
+              </div>
+              <div class="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">
+                    <tr>
+                      <th class="py-2.5 px-3">Address</th>
+                      <th class="py-2.5 px-3">Gateway</th>
+                      <th class="py-2.5 px-3">Netmask</th>
+                      <th class="py-2.5 px-3">DNS Servers</th>
+                      <th class="py-2.5 px-3">Domain</th>
+                      <th class="py-2.5 px-3">NTP Servers (Opt 42)</th>
+                      <th class="py-2.5 px-3">Next Server (TFTP)</th>
+                      <th class="py-2.5 px-3">Boot File (PXE)</th>
+                      <th class="py-2.5 px-3">Comment</th>
+                      <th class="py-2.5 px-3 text-right font-sans">Actions</th>
+                    </tr>
+                  </thead>
+                  <tbody id="dhcp-networks-tbody" class="divide-y divide-slate-800/80"></tbody>
+                </table>
+              </div>
+              <div id="dhcp-networks-empty" class="hidden p-10 text-center text-slate-500 text-xs font-sans">
+                No DHCP networks configured. Click "+ Add Network" to configure subnet parameters.
+              </div>
+            </div>
+          </div>
+
+          <!-- SUBTAB 3: LEASES (/ip dhcp-server lease) -->
+          <div id="dhcp-server-subtab-leases" class="hidden space-y-4">
+            <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>
+                  <h3 class="text-xs font-bold text-white">DHCP Leases & Reservations (/ip dhcp-server lease)</h3>
+                  <p class="text-[11px] text-slate-400 mt-0.5">Active dynamic leases and static MAC-to-IP reservations</p>
+                </div>
+                <div class="flex items-center gap-2">
+                  <span class="text-xs text-slate-400 font-mono">
+                    <span class="text-sky-400">D</span> = Dynamic &nbsp;|&nbsp; <span class="text-purple-400">S</span> = Static &nbsp;|&nbsp; <span class="text-rose-400">X</span> = Disabled
+                  </span>
+                  <button onclick="openAddDHCPStaticLeaseModal()" class="text-xs font-semibold px-3 py-1.5 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white transition">
+                    + Add Static Lease
+                  </button>
+                </div>
+              </div>
+              <div class="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">
+                    <tr>
+                      <th class="py-2.5 px-3 w-12 text-center">Flag</th>
+                      <th class="py-2.5 px-3">Address</th>
+                      <th class="py-2.5 px-3">MAC Address</th>
+                      <th class="py-2.5 px-3">Host Name</th>
+                      <th class="py-2.5 px-3">Server</th>
+                      <th class="py-2.5 px-3">Expires After</th>
+                      <th class="py-2.5 px-3">Status</th>
+                      <th class="py-2.5 px-3">Comment</th>
+                      <th class="py-2.5 px-3 text-right font-sans">Actions</th>
+                    </tr>
+                  </thead>
+                  <tbody id="dhcp-leases-tbody" class="divide-y divide-slate-800/80"></tbody>
+                </table>
+              </div>
+              <div id="dhcp-leases-empty" class="hidden p-10 text-center text-slate-500 text-xs font-sans">
+                No active leases or static reservations found.
+              </div>
+            </div>
+          </div>
+
+          <!-- SUBTAB 4: OPTIONS (/ip dhcp-server option) -->
+          <div id="dhcp-server-subtab-options" class="hidden space-y-4">
+            <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>
+                  <h3 class="text-xs font-bold text-white">Custom DHCP Options (/ip dhcp-server option)</h3>
+                  <p class="text-[11px] text-slate-400 mt-0.5">Standard & Vendor DHCP Options (NTP, TFTP, UniFi Inform, CAPWAP, VoIP)</p>
+                </div>
+                <button onclick="openAddDHCPOptionModal()" class="text-xs font-semibold px-3 py-1.5 rounded-lg bg-emerald-600 hover:bg-emerald-500 text-white transition">
+                  + Add Option
+                </button>
+              </div>
+              <div class="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">
+                    <tr>
+                      <th class="py-2.5 px-3">Name</th>
+                      <th class="py-2.5 px-3">Code</th>
+                      <th class="py-2.5 px-3">Value</th>
+                      <th class="py-2.5 px-3">Comment</th>
+                      <th class="py-2.5 px-3 text-right font-sans">Actions</th>
+                    </tr>
+                  </thead>
+                  <tbody id="dhcp-options-tbody" class="divide-y divide-slate-800/80"></tbody>
+                </table>
+              </div>
+              <div id="dhcp-options-empty" class="hidden p-10 text-center text-slate-500 text-xs font-sans">
+                No custom DHCP options configured.
+              </div>
+            </div>
+          </div>
+        </section>
+
         <!-- ================= SECTION: BRIDGE (/interface bridge) ================= -->
         <section id="section-bridge" class="hidden space-y-6">
           <!-- Bridge Navigation Header -->
@@ -9333,6 +9584,336 @@
     </div>
   </div>
 
+  <!-- ================= MODAL: ADD / EDIT DHCP SERVER INSTANCE ================= -->
+  <div id="dhcp-server-instance-modal" 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/40">
+        <div class="flex items-center gap-2">
+          <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">/ip dhcp-server</span>
+          <h3 id="dhcp-server-modal-title" class="text-sm font-bold text-white">Add DHCP Server</h3>
+        </div>
+        <button onclick="closeDHCPServerModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
+      </div>
+
+      <form onsubmit="submitSaveDHCPServer(event)" class="p-6 space-y-4 text-xs font-sans">
+        <input type="hidden" id="dhcp-server-modal-id">
+
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Name *</label>
+            <input type="text" id="dhcp-server-modal-name" placeholder="dhcp1" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Interface *</label>
+            <select id="dhcp-server-modal-iface" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+              <option value="">Select interface...</option>
+            </select>
+          </div>
+        </div>
+
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Address Pool (Range) *</label>
+            <input type="text" id="dhcp-server-modal-pool" placeholder="192.168.88.10-192.168.88.254" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+            <span class="text-[10px] text-slate-400">Format: startIP-endIP</span>
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Lease Time</label>
+            <input type="text" id="dhcp-server-modal-leasetime" value="12h" placeholder="12h, 1d, 30m" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+            <span class="text-[10px] text-slate-400">Default: 12h</span>
+          </div>
+        </div>
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1.5">Comment</label>
+          <input type="text" id="dhcp-server-modal-comment" placeholder="LAN Office Pool" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white focus:outline-none focus:border-emerald-500">
+        </div>
+
+        <div class="flex items-center justify-between pt-2 border-t border-slate-800/80">
+          <label class="flex items-center space-x-2 text-slate-300 cursor-pointer">
+            <input type="checkbox" id="dhcp-server-modal-authoritative" checked class="rounded bg-slate-800 border-slate-700 text-emerald-600">
+            <span>Authoritative Mode (Fast NAK for stale clients)</span>
+          </label>
+          <label class="flex items-center space-x-2 text-slate-300 cursor-pointer">
+            <input type="checkbox" id="dhcp-server-modal-disabled" class="rounded bg-slate-800 border-slate-700 text-rose-600">
+            <span>Disabled (X)</span>
+          </label>
+        </div>
+
+        <div class="pt-4 border-t border-slate-800 flex justify-end space-x-3">
+          <button type="button" onclick="closeDHCPServerModal()" 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 Server</button>
+        </div>
+      </form>
+    </div>
+  </div>
+
+  <!-- ================= MODAL: ADD / EDIT DHCP NETWORK ================= -->
+  <div id="dhcp-network-modal" 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-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/40">
+        <div class="flex items-center gap-2">
+          <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">/ip dhcp-server network</span>
+          <h3 id="dhcp-network-modal-title" class="text-sm font-bold text-white">Add DHCP Network</h3>
+        </div>
+        <button onclick="closeDHCPNetworkModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
+      </div>
+
+      <form onsubmit="submitSaveDHCPNetwork(event)" class="p-6 space-y-4 text-xs font-sans max-h-[80vh] overflow-y-auto">
+        <input type="hidden" id="dhcp-network-modal-id">
+
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Subnet Address (CIDR) *</label>
+            <input type="text" id="dhcp-network-modal-address" placeholder="192.168.88.0/24" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+            <span class="text-[10px] text-slate-400">Network address with CIDR mask</span>
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Gateway (Router - Option 3) *</label>
+            <input type="text" id="dhcp-network-modal-gateway" placeholder="192.168.88.1" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+            <span class="text-[10px] text-slate-400">Default gateway advertised to clients</span>
+          </div>
+        </div>
+
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">DNS Servers (Option 6)</label>
+            <input type="text" id="dhcp-network-modal-dns" placeholder="192.168.88.1, 1.1.1.1, 8.8.8.8" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+            <span class="text-[10px] text-slate-400">Comma-separated list of DNS IPs</span>
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Domain Name (Option 15)</label>
+            <input type="text" id="dhcp-network-modal-domain" placeholder="lan or company.local" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+            <span class="text-[10px] text-slate-400">Domain search suffix</span>
+          </div>
+        </div>
+
+        <div class="p-3 bg-slate-950/60 border border-slate-800 rounded-xl space-y-3">
+          <div class="text-[11px] font-bold text-emerald-400 uppercase tracking-wider 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 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
+            <span>Time & Network Boot Options (NTP & TFTP/PXE)</span>
+          </div>
+
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1">NTP Servers (Option 42)</label>
+            <input type="text" id="dhcp-network-modal-ntp" placeholder="192.168.88.1, time.cloudflare.com, pool.ntp.org" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+            <span class="text-[10px] text-slate-400">Comma-separated NTP time servers distributed to DHCP clients</span>
+          </div>
+
+          <div class="grid grid-cols-2 gap-3">
+            <div>
+              <label class="block font-semibold text-slate-300 mb-1">Next Server / TFTP Server (Option 66)</label>
+              <input type="text" id="dhcp-network-modal-tftp" placeholder="192.168.88.50" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+              <span class="text-[10px] text-slate-400">TFTP server IP/host (siaddr / Next-Server)</span>
+            </div>
+            <div>
+              <label class="block font-semibold text-slate-300 mb-1">Boot File Name / PXE (Option 67)</label>
+              <input type="text" id="dhcp-network-modal-bootfile" placeholder="pxelinux.0 or ipxe.efi" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+              <span class="text-[10px] text-slate-400">Network bootstrap file (file / Bootfile)</span>
+            </div>
+          </div>
+        </div>
+
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">WINS Servers (Option 44)</label>
+            <input type="text" id="dhcp-network-modal-wins" placeholder="192.168.88.50" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Netmask (Optional Override)</label>
+            <input type="text" id="dhcp-network-modal-netmask" placeholder="255.255.255.0" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+          </div>
+        </div>
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1.5">Linked Custom DHCP Options</label>
+          <div id="dhcp-network-modal-options-list" class="grid grid-cols-2 gap-2 p-2 bg-slate-950/60 border border-slate-800 rounded-lg max-h-32 overflow-y-auto font-mono text-[11px]">
+            <!-- Dynamically populated options checkboxes -->
+          </div>
+        </div>
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1.5">Comment</label>
+          <input type="text" id="dhcp-network-modal-comment" placeholder="Office LAN Subnet" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white focus:outline-none focus:border-emerald-500">
+        </div>
+
+        <div class="pt-4 border-t border-slate-800 flex justify-end space-x-3">
+          <button type="button" onclick="closeDHCPNetworkModal()" 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 Network</button>
+        </div>
+      </form>
+    </div>
+  </div>
+
+  <!-- ================= MODAL: ADD / EDIT STATIC LEASE ================= -->
+  <div id="dhcp-static-lease-modal" 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/40">
+        <div class="flex items-center gap-2">
+          <span class="px-2 py-0.5 rounded text-[10px] font-mono bg-purple-500/20 text-purple-300 border border-purple-500/30 font-semibold">/ip dhcp-server lease</span>
+          <h3 id="dhcp-static-lease-modal-title" class="text-sm font-bold text-white">Add Static Lease Reservation</h3>
+        </div>
+        <button onclick="closeDHCPStaticLeaseModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
+      </div>
+
+      <form onsubmit="submitSaveDHCPStaticLease(event)" class="p-6 space-y-4 text-xs font-sans">
+        <input type="hidden" id="dhcp-static-lease-modal-id">
+
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">MAC Address *</label>
+            <input type="text" id="dhcp-static-lease-modal-mac" placeholder="AA:BB:CC:DD:EE:FF" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-purple-500">
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Reserved IP Address *</label>
+            <input type="text" id="dhcp-static-lease-modal-ip" placeholder="192.168.88.100" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-purple-500">
+          </div>
+        </div>
+
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Host Name</label>
+            <input type="text" id="dhcp-static-lease-modal-hostname" placeholder="printer, nas, camera" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-purple-500">
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Server</label>
+            <input type="text" id="dhcp-static-lease-modal-server" value="all" placeholder="all or server name" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-purple-500">
+          </div>
+        </div>
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1.5">Comment</label>
+          <input type="text" id="dhcp-static-lease-modal-comment" placeholder="LaserJet Pro Printer" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white focus:outline-none focus:border-purple-500">
+        </div>
+
+        <div class="pt-2 border-t border-slate-800/80">
+          <label class="flex items-center space-x-2 text-slate-300 cursor-pointer">
+            <input type="checkbox" id="dhcp-static-lease-modal-disabled" class="rounded bg-slate-800 border-slate-700 text-rose-600">
+            <span>Disabled (X)</span>
+          </label>
+        </div>
+
+        <div class="pt-4 border-t border-slate-800 flex justify-end space-x-3">
+          <button type="button" onclick="closeDHCPStaticLeaseModal()" 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-purple-600 hover:bg-purple-500 text-white shadow-lg shadow-purple-500/20">Save Reservation</button>
+        </div>
+      </form>
+    </div>
+  </div>
+
+  <!-- ================= MODAL: ADD / EDIT DHCP OPTION ================= -->
+  <div id="dhcp-option-modal" 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-md 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 class="flex items-center gap-2">
+          <span class="px-2 py-0.5 rounded text-[10px] font-mono bg-cyan-500/20 text-cyan-300 border border-cyan-500/30 font-semibold">/ip dhcp-server option</span>
+          <h3 id="dhcp-option-modal-title" class="text-sm font-bold text-white">Add DHCP Option</h3>
+        </div>
+        <button onclick="closeDHCPOptionModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
+      </div>
+
+      <form onsubmit="submitSaveDHCPOption(event)" class="p-6 space-y-4 text-xs font-sans">
+        <input type="hidden" id="dhcp-option-modal-id">
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1.5">Option Name *</label>
+          <input type="text" id="dhcp-option-modal-name" placeholder="unifi-inform" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-cyan-500">
+        </div>
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1.5">Option Code (1 - 254) *</label>
+          <input type="number" id="dhcp-option-modal-code" min="1" max="254" placeholder="43" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-cyan-500">
+          <span class="text-[10px] text-slate-400">e.g. 42 (NTP), 43 (Vendor), 66 (TFTP), 67 (Bootfile), 121 (Routes), 150 (VoIP)</span>
+        </div>
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1.5">Option Value *</label>
+          <input type="text" id="dhcp-option-modal-value" placeholder="01:04:c0:a8:58:0a or 192.168.1.10" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-cyan-500">
+          <span class="text-[10px] text-slate-400">Format: IP address, comma-separated list, text in quotes, or raw colon-separated hex</span>
+        </div>
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1.5">Comment</label>
+          <input type="text" id="dhcp-option-modal-comment" placeholder="UniFi Network Controller inform IP" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white focus:outline-none focus:border-cyan-500">
+        </div>
+
+        <div class="pt-4 border-t border-slate-800 flex justify-end space-x-3">
+          <button type="button" onclick="closeDHCPOptionModal()" 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-cyan-600 hover:bg-cyan-500 text-white shadow-lg shadow-cyan-500/20">Save Option</button>
+        </div>
+      </form>
+    </div>
+  </div>
+
+  <!-- ================= MODAL: MIKROTIK DHCP SETUP WIZARD ================= -->
+  <div id="dhcp-setup-wizard-modal" 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/40">
+        <div class="flex items-center gap-2">
+          <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">DHCP Setup</span>
+          <h3 class="text-sm font-bold text-white">DHCP Server Setup Wizard</h3>
+        </div>
+        <button onclick="closeDHCPServerSetupModal()" class="text-slate-400 hover:text-white p-1 rounded-lg">✕</button>
+      </div>
+
+      <form onsubmit="submitDHCPServerSetupWizard(event)" class="p-6 space-y-4 text-xs font-sans">
+        <p class="text-slate-400 text-xs">
+          Select an interface to automatically configure a complete MikroTik-style DHCP Server, network subnet, IP address pool, default gateway, and DNS.
+        </p>
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1.5">DHCP Server Interface *</label>
+          <select id="dhcp-wizard-iface" onchange="onDHCPWizardIfaceChange()" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+            <option value="">Select interface...</option>
+          </select>
+        </div>
+
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">DHCP Address Space</label>
+            <input type="text" id="dhcp-wizard-space" placeholder="192.168.88.0/24" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Gateway for DHCP Network</label>
+            <input type="text" id="dhcp-wizard-gateway" placeholder="192.168.88.1" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+          </div>
+        </div>
+
+        <div>
+          <label class="block font-semibold text-slate-300 mb-1.5">Addresses to Give Out (Address Pool)</label>
+          <input type="text" id="dhcp-wizard-pool" placeholder="192.168.88.10-192.168.88.254" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+        </div>
+
+        <div class="grid grid-cols-2 gap-4">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">DNS Servers</label>
+            <input type="text" id="dhcp-wizard-dns" placeholder="192.168.88.1, 1.1.1.1" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1.5">Lease Time</label>
+            <input type="text" id="dhcp-wizard-leasetime" value="12h" placeholder="12h" required class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+          </div>
+        </div>
+
+        <div class="grid grid-cols-2 gap-4 pt-1">
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1">NTP Server (Optional)</label>
+            <input type="text" id="dhcp-wizard-ntp" placeholder="192.168.88.1" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+          </div>
+          <div>
+            <label class="block font-semibold text-slate-300 mb-1">TFTP Server / PXE Boot (Optional)</label>
+            <input type="text" id="dhcp-wizard-tftp" placeholder="192.168.88.50" class="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white font-mono focus:outline-none focus:border-emerald-500">
+          </div>
+        </div>
+
+        <div class="pt-4 border-t border-slate-800 flex justify-end space-x-3">
+          <button type="button" onclick="closeDHCPServerSetupModal()" 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">Finish Setup</button>
+        </div>
+      </form>
+    </div>
+  </div>
+
   <!-- ================= MODAL: ADD / EDIT BRIDGE ================= -->
   <div id="bridge-modal" 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">
@@ -9578,6 +10159,6 @@
   </div>
 
   <datalist id="fw-addrlist-datalist"></datalist>
-  <script src="app.js?v=8.1"></script>
+  <script src="app.js?v=9.0"></script>
 </body>
 </html>