Browse Source

BindManager Final Release

root 6 months ago
parent
commit
84b4655423

+ 18 - 24
app.py

@@ -58,15 +58,16 @@ def user_api_required(f):
     @wraps(f)
     def dec(*args, **kwargs):
         token = request.headers.get('Authorization') # Expected: Bearer <token>
-        if token and token.startswith('Bearer '):
+        # FIX: Ασφαλές split για να μην πετάει IndexError 500
+        if token and token.startswith('Bearer ') and len(token.split(' ')) > 1:
             token = token.split(' ')[1]
-        
-        users = fm.load_users()
-        for username, data in users.items():
-            if data.get('api_token') == token:
-                session['username'] = username
-                session['role'] = data.get('role')
-                return f(*args, **kwargs)
+            users = fm.load_users()
+            for username, data in users.items():
+                if data.get('api_token') == token:
+                    session['username'] = username
+                    session['role'] = data.get('role')
+                    return f(*args, **kwargs)
+                    
         return jsonify({"status": "error", "message": "Invalid or missing API Token"}), 401
     return dec
 
@@ -240,7 +241,8 @@ def delete_zone(domain):
     if not check_zone_access(clean): return redirect(url_for('dashboard'))
     
     conf = fm.read_file(config.LOCAL_CONF)
-    path = dns.get_zone_file_path(clean)
+    # FIX: Παίρνουμε το path ΠΡΙΝ σβήσουμε το block από το conf!
+    path = dns.get_zone_file_path(clean) 
     block = dns.get_zone_block(conf, clean)
     
     if block:
@@ -249,8 +251,11 @@ def delete_zone(domain):
         s['zone_owners'].pop(clean, None)
         fm.save_panel_settings(s)
         subprocess.run(["rndc", "reconfig"])
+        
+        # Τώρα που έχουμε το σωστό path, το σβήνουμε με ασφάλεια.
         try: os.remove(path)
         except: pass
+        
         if "type master" in block: cm.broadcast_to_cluster('delete_zone', clean)
         
     flash("Zone Deleted", "success")
@@ -597,9 +602,7 @@ logging {
     s.update({"templates": new_templates, "stats_retention": int(request.form.get('stats_retention', 30)), "query_log_enabled": query_log})
     fm.save_panel_settings(s)
     
-    # -- Η ΠΡΟΣΘΗΚΗ ΠΟΥ ΕΛΕΙΠΕ: Στέλνει τα Templates στους Slaves! --
     cm.broadcast_templates_to_cluster(new_templates)
-    # ---------------------------------------------------------------
     
     subprocess.run(["systemctl", "restart", config.BIND_SERVICE])
     flash("Settings Saved", "success")
@@ -631,13 +634,13 @@ def save_user():
         if u in users: flash("User exists", "error")
         else: 
             users[u] = {"password": generate_password_hash(p), "role": r, "assigned_templates": assigned, "view_mode": "cards"}
-            cm.broadcast_user_to_cluster('save', u, users[u]) # CLUSTER SYNC
+            cm.broadcast_user_to_cluster('save', u, users[u])
     elif action == 'edit' and old_u in users:
         pw = generate_password_hash(p) if p else users[old_u]['password']
         users[old_u] = {"password": pw, "role": r, "assigned_templates": assigned, "view_mode": users[old_u].get('view_mode','cards'), "api_token": users[old_u].get('api_token')}
         if u != old_u: 
             users[u] = users.pop(old_u)
-        cm.broadcast_user_to_cluster('save', u, users[u], old_username=old_u if u != old_u else None) # CLUSTER SYNC
+        cm.broadcast_user_to_cluster('save', u, users[u], old_username=old_u if u != old_u else None)
         
     fm.save_users(users)
     flash("User saved", "success")
@@ -650,7 +653,7 @@ def delete_user(username):
     if username != 'admin' and username in users:
         del users[username]
         fm.save_users(users)
-        cm.broadcast_user_to_cluster('delete', username) # CLUSTER SYNC
+        cm.broadcast_user_to_cluster('delete', username)
         flash("User deleted", "success")
     return redirect(url_for('manage_users'))
 
@@ -660,7 +663,6 @@ def delete_user(username):
 def route_update_adblock():
     result = ab.generate_and_apply_rpz()
     if result.get("status") == "success":
-        # BUG FIX: Ειδοποιούμε το Cluster ότι κάναμε update!
         cm.broadcast_adblock_to_cluster('enable')
         return jsonify(result)
     else:
@@ -914,7 +916,6 @@ def export_backup():
         if os.path.exists(f): 
             zip_f.write(f, os.path.basename(f))
             
-    # BUG FIX: Διπλός έλεγχος os.path.exists() για να μην κρασάρει το zip
     for f in os.listdir(config.BIND_PATH):
         if f.startswith("db.") and f != "db.rpz.blacklist": 
             full_path = os.path.join(config.BIND_PATH, f)
@@ -988,14 +989,12 @@ def api_add_slave():
     
     conf = fm.read_file(config.LOCAL_CONF)
     
-    # --- OVERWRITE LOGIC ---
     old_block = dns.get_zone_block(conf, domain)
     if old_block:
         conf = conf.replace(old_block, "")
         message = f"Zone {domain} overwritten"
     else:
         message = f"Zone {domain} added"
-    # -----------------------
         
     path = os.path.join(config.CACHE_PATH, f"db.{domain}")
     new_zone = f'\nzone "{domain}" {{\n    type slave;\n    file "{path}";\n    masters {{{masters_string} }};\n    masterfile-format text;\n}};\n'
@@ -1194,7 +1193,7 @@ def generate_user_token():
     new_token = secrets.token_urlsafe(32)
     users[session['username']]['api_token'] = new_token
     fm.save_users(users)
-    cm.broadcast_user_to_cluster('save', session['username'], users[session['username']]) # CLUSTER SYNC
+    cm.broadcast_user_to_cluster('save', session['username'], users[session['username']])
     flash("New API Token generated successfully!", "success")
     return redirect(url_for('profile'))
 
@@ -1205,7 +1204,6 @@ def api_user_check_zone(domain):
     exists = f'zone "{domain}"' in conf
     return jsonify({"status": "success", "exists": exists, "domain": domain})
 
-# --- 3. API CREATE ZONE (Update) ---
 @app.route('/api/v1/user/zone/create', methods=['POST'])
 @user_api_required
 def api_user_create_zone():
@@ -1221,7 +1219,6 @@ def api_user_create_zone():
         return jsonify({"status": "error", "message": "Zone already exists"}), 409
 
     if z_type == 'master':
-        # MANDATORY TEMPLATE CHECK
         if not t_name:
             return jsonify({"status": "error", "message": "template_name is mandatory for Master zones"}), 400
             
@@ -1239,7 +1236,6 @@ def api_user_create_zone():
         fm.write_file(path, dns.apply_template(t['content'], domain, t))
         
     else:
-        # (Αν θελήσεις αργότερα να επιτρέπεις στο API να φτιάχνει και slaves)
         return jsonify({"status": "error", "message": "Only Master zones are supported via this API endpoint currently."}), 400
     
     fm.write_file(config.LOCAL_CONF, new_conf)
@@ -1267,11 +1263,9 @@ def api_user_add_record():
     path = dns.get_zone_file_path(domain)
     content = fm.read_file(path)
     
-    # Sanitization and proper formatting (e.g. TXT quotes, CNAME dots)
     clean_name = dns.sanitize_record_name(name, domain)
     val = dns.construct_record_value({'value': value}, rtype)
     
-    # Check if exists
     if f"{clean_name}\tIN\t{rtype}\t{val}" in content:
         return jsonify({"status": "error", "message": "Record already exists"}), 409
         

BIN
core/__pycache__/adblock_manager.cpython-39.pyc


BIN
core/__pycache__/dns_manager.cpython-39.pyc


BIN
core/__pycache__/file_manager.cpython-39.pyc


+ 20 - 14
core/adblock_manager.py

@@ -12,7 +12,7 @@ from core.dns_manager import get_zone_block
 def is_valid_domain(d):
     if not d or len(d) > 253: return False
     if '..' in d or d.startswith('.'): return False
-    # Επιτρέπει μόνο αλφαριθμητικά, παύλες και τελείες. Απορρίπτει *, |, /, ?, $ κλπ.
+    # Επιτρέπει μόνο αλφαριθμητικά, παύλες, underscores και τελείες.
     if not re.match(r'^[a-zA-Z0-9\-_.]+$', d): return False
     if '.' not in d: return False
     return True
@@ -32,11 +32,11 @@ def generate_and_apply_rpz():
                 with urllib.request.urlopen(req, timeout=15, context=config.ssl_ctx) as response:
                     lines = response.read().decode('utf-8', errors='ignore').splitlines()
                     for line in lines:
-                        l = line.strip()
-                        # Αγνοούμε σχόλια και άκυρες γραμμές
-                        if not l or l.startswith(('#', '!', ':', '[', '@')): continue
-                        # Αγνοούμε URL paths (π.χ. example.com/ad.gif)
-                        if '/' in l: continue 
+                        l = line.split('#')[0].strip() # Κόβουμε inline comments
+                        
+                        # Γρήγορα φίλτρα (Αγνοούμε σχόλια, HTML, και Regex τύπου Pi-hole)
+                        if not l or l.startswith(('!', ':', '[', '@')): continue
+                        if '/' in l or '*' in l or '|' in l and not l.startswith('||'): continue
                         
                         d = ""
                         if l.startswith('||'):
@@ -49,14 +49,14 @@ def generate_and_apply_rpz():
                             d = re.split(r'[\^/\$:]', l)[0]
                             
                         d = d.strip().rstrip('.')
-                        # Strict Validation
+                        # Αυστηρός Έλεγχος (Strict Validation)
                         if is_valid_domain(d):
                             domains_set.add(d)
             except Exception as e:
                 print(f"Failed to fetch {lst['name']}: {e}")
 
         # 2. Add Custom Deny
-        for d in custom_rules['deny']:
+        for d in custom_rules.get('deny', []):
             clean_d = d.strip().rstrip('.')
             if clean_d.startswith('*.'): clean_d = clean_d[2:]
             if is_valid_domain(clean_d):
@@ -65,22 +65,28 @@ def generate_and_apply_rpz():
         # 3. Create RPZ
         rpz_records = []
         
-        # ΠΡΩΤΑ τα Allowed (PASSTHRU)
-        for d in custom_rules['allowed']:
+        # Μετατροπή allowed λίστας σε SET + καθάρισμα (Για ακαριαία ταχύτητα O(1))
+        allowed_cleaned = set()
+        for d in custom_rules.get('allowed', []):
             clean_d = d.strip().rstrip('.')
             if clean_d.startswith('*.'): clean_d = clean_d[2:]
             if is_valid_domain(clean_d):
-                rpz_records.append(f"{clean_d}\tCNAME\trpz-passthru.")
-                rpz_records.append(f"*.{clean_d}\tCNAME\trpz-passthru.")
+                allowed_cleaned.add(clean_d)
+        
+        # ΠΡΩΤΑ τα Allowed (PASSTHRU)
+        for d in allowed_cleaned:
+            rpz_records.append(f"{d}\tCNAME\trpz-passthru.")
+            rpz_records.append(f"*.{d}\tCNAME\trpz-passthru.")
 
         # ΜΕΤΑ τα Blocked
         for d in domains_set:
-            if d in custom_rules['allowed'] or f"*.{d}" in custom_rules['allowed']:
+            # Ακαριαίος έλεγχος χάρη στο set()
+            if d in allowed_cleaned:
                 continue
             rpz_records.append(f"{d}\tCNAME\t.")
             rpz_records.append(f"*.{d}\tCNAME\t.")
 
-        serial = time.strftime('%Y%m%d01')
+        serial = time.strftime('%Y%m%d%H') # Ώρα αντί για 01 για σίγουρο increment
         header = f"$TTL 60\n$ORIGIN rpz.blacklist.\n@ IN SOA localhost. root.localhost. ({serial} 3h 1h 1w 1h)\n IN NS localhost.\n"
         content = header + "\n".join(rpz_records) + "\n"
         

+ 28 - 8
core/dns_manager.py

@@ -3,6 +3,7 @@ import re
 import time
 import datetime
 import subprocess
+import secrets
 import config
 from core.file_manager import read_file, write_file
 
@@ -22,10 +23,13 @@ def get_backups(domain):
     return backups
 
 def check_zone_syntax(domain, content):
-    tmp_path = os.path.join("/tmp", f"bind_check_{domain}_{int(time.time())}.db")
+    # FIX 3: Μοναδικό temp file για να μην υπάρξει ποτέ σύγκρουση (Race Condition)
+    tmp_path = os.path.join("/tmp", f"bind_check_{domain}_{secrets.token_hex(4)}.db")
     write_file(tmp_path, content)
     res = subprocess.run(["named-checkzone", domain, tmp_path], capture_output=True, text=True)
-    os.remove(tmp_path)
+    try: os.remove(tmp_path)
+    except: pass
+    
     if res.returncode == 0: return True, ""
     else: return False, res.stdout.replace(tmp_path, 'zone_file').strip()
 
@@ -73,7 +77,6 @@ def get_zone_block(conf, domain):
             block = 'zone "' + p
             open_braces = 0
             in_block = False
-            # Μετράμε αγκύλια για να βρούμε το τέλος της ζώνης με ακρίβεια
             for i, char in enumerate(block):
                 if char == '{': 
                     open_braces += 1
@@ -115,11 +118,15 @@ def parse_zone_file(path, domain=None):
         ridx = -1
         for i, t in enumerate(tokens):
             if t.upper() in records: ridx = i; break
+            
         if ridx != -1:
             rtype = tokens[ridx].upper()
             name = re.sub(r'\.{2,}', '.', tokens[0])
             if name.upper() in ['IN', '@'] or name.isdigit(): name = "@"
-            val = clean.split(tokens[ridx], 1)[1].strip() if rtype == 'TXT' else " ".join(tokens[ridx+1:])
+            
+            # FIX 2: Αλεξίσφαιρη ανάγνωση της τιμής χωρίς να σπάει αν το name περιέχει "TXT" ή "A"
+            match = re.search(rf'\s+(?:IN\s+)?{rtype}\s+(.*)', clean, re.IGNORECASE)
+            val = match.group(1).strip() if match else " ".join(tokens[ridx+1:])
             
             friendly_name = name
             if is_reverse and name != '@':
@@ -131,11 +138,24 @@ def parse_zone_file(path, domain=None):
     return records
 
 def increment_serial(content):
-    match = re.search(r'(\d+)\s*;\s*Serial', content)
+    # FIX 1 (CRITICAL): Έξυπνος SOA Serial Parser
+    # Αντί να ψάχνει σχόλια, βρίσκει το "SOA", ανοίγει την παρένθεση "(" και αρπάζει τον 1ο αριθμό που θα βρει!
+    match = re.search(r'(SOA\s+.*?\(\s*)(\d+)', content, re.IGNORECASE | re.DOTALL)
+    
     if match:
-        old_serial = match.group(1)
-        new_serial = str(int(old_serial) + 1)
-        return content.replace(old_serial, new_serial)
+        prefix = match.group(1)
+        old_serial = match.group(2)
+        
+        today_prefix = time.strftime('%Y%m%d')
+        # Αν το serial είναι ήδη σημερινό (π.χ. 2026042001), του προσθέτουμε +1
+        if old_serial.startswith(today_prefix):
+            new_serial = str(int(old_serial) + 1)
+        else:
+            # Αλλιώς ξεκινάμε με σημερινή ημερομηνία + 01
+            new_serial = today_prefix + "01"
+            
+        return content[:match.start()] + prefix + new_serial + content[match.end():]
+        
     return content
 
 def construct_record_value(req_form, rtype):

+ 156 - 53
templates/dashboard.html

@@ -93,15 +93,15 @@
                 </div>
                 <div class="flex gap-3 opacity-0 group-hover:opacity-100 transition items-center">
                     {% if session.get('role') == 'admin' %}
-                    <button onclick="openAssignModal('{{ zone_name }}', '{{ owners.get(zone_name, 'admin') }}')" class="text-blue-400 text-[10px] font-black uppercase tracking-widest hover:text-blue-300">Assign</button>
+                    <button type="button" onclick="openAssignModal('{{ zone_name|e }}', '{{ owners.get(zone_name, 'admin')|e }}')" class="text-blue-400 text-[10px] font-black uppercase tracking-widest hover:text-blue-300">Assign</button>
                     {% endif %}
-                    <a href="/zones/delete/{{ url_param }}" onclick="handleAjaxLink(event, this, 'dashboard-dynamic-content', restoreDashboardState, 'Delete this zone permanently?')" class="text-red-500 text-[10px] font-black uppercase tracking-widest hover:text-red-400 transition">Delete</a>
+                    <a href="/zones/delete/{{ url_param|urlencode }}" onclick="handleAjaxLink(event, this, 'dashboard-dynamic-content', restoreDashboardState, 'Delete this zone permanently?')" class="text-red-500 text-[10px] font-black uppercase tracking-widest hover:text-red-400 transition">Delete</a>
                 </div>
             </div>
             <h3 class="text-xl font-bold text-white mb-6 font-mono truncate" title="{{ zone_name }}">
                 {% if zone_name == '.' %}. (Root Zone){% else %}{{ zone_name }}{% endif %}
             </h3>
-            <a href="/zones/manage/{{ url_param }}" class="block w-full text-center bg-gray-800 hover:bg-blue-600 py-3 rounded-xl text-gray-300 hover:text-white font-black text-xs transition uppercase tracking-widest">Manage</a>
+            <a href="/zones/manage/{{ url_param|urlencode }}" class="block w-full text-center bg-gray-800 hover:bg-blue-600 py-3 rounded-xl text-gray-300 hover:text-white font-black text-xs transition uppercase tracking-widest">Manage</a>
         </div>
         {% endfor %}
     </div>
@@ -124,9 +124,11 @@
                     <td class="p-6 text-gray-400 text-xs uppercase">{{ owners.get(zone_name, 'admin') }}</td>
                     <td class="p-6 text-right">
                         <div class="flex justify-end gap-4 items-center">
-                            {% if session.get('role') == 'admin' %}<button onclick="openAssignModal('{{ zone_name }}', '{{ owners.get(zone_name, 'admin') }}')" class="opacity-0 group-hover:opacity-100 text-purple-400 text-[10px] font-black uppercase transition">Assign</button>{% endif %}
-                            <a href="/zones/manage/{{ url_param }}" class="text-green-400 font-black text-[10px] uppercase">Manage</a>
-                            <a href="/zones/delete/{{ url_param }}" onclick="handleAjaxLink(event, this, 'dashboard-dynamic-content', restoreDashboardState, 'Delete this zone permanently?')" class="opacity-0 group-hover:opacity-100 text-red-500 text-[10px] font-black uppercase transition">Delete</a>
+                            {% if session.get('role') == 'admin' %}
+                            <button type="button" onclick="openAssignModal('{{ zone_name|e }}', '{{ owners.get(zone_name, 'admin')|e }}')" class="opacity-0 group-hover:opacity-100 text-purple-400 text-[10px] font-black uppercase transition">Assign</button>
+                            {% endif %}
+                            <a href="/zones/manage/{{ url_param|urlencode }}" class="text-green-400 font-black text-[10px] uppercase">Manage</a>
+                            <a href="/zones/delete/{{ url_param|urlencode }}" onclick="handleAjaxLink(event, this, 'dashboard-dynamic-content', restoreDashboardState, 'Delete this zone permanently?')" class="opacity-0 group-hover:opacity-100 text-red-500 text-[10px] font-black uppercase transition">Delete</a>
                         </div>
                     </td>
                 </tr>
@@ -142,9 +144,18 @@
     <div class="bg-[#12161f] border border-gray-800 w-full max-w-sm rounded-3xl p-8 shadow-2xl">
         <h3 class="text-xl font-black text-white mb-6 uppercase text-center">Assign Ownership</h3>
         <form action="/zones/assign" method="POST" class="space-y-6" onsubmit="handleAjaxForm(event, this, 'dashboard-dynamic-content', restoreDashboardState)">
-            <input type="hidden" name="domain" id="assignDomainInput"><div class="text-center mb-4"><span id="assignDomainDisplay" class="text-lg font-mono text-blue-400 font-bold"></span></div>
-            <select name="owner" id="assignOwnerSelect" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-4 text-white font-bold outline-none cursor-pointer"><option value="admin">admin (Unassign)</option>{% for u in all_usernames %}{% if u != 'admin' %}<option value="{{ u }}">{{ u }}</option>{% endif %}{% endfor %}</select>
-            <div class="flex gap-4 pt-6"><button type="button" onclick="closeAssignModal()" class="flex-1 bg-gray-800 text-gray-400 font-bold py-4 rounded-xl uppercase text-xs">Cancel</button><button type="submit" class="flex-1 bg-blue-600 text-white font-black py-4 rounded-xl uppercase text-xs shadow-xl">Assign</button></div>
+            <input type="hidden" name="domain" id="assignDomainInput">
+            <div class="text-center mb-4"><span id="assignDomainDisplay" class="text-lg font-mono text-blue-400 font-bold"></span></div>
+            <select name="owner" id="assignOwnerSelect" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-4 text-white font-bold outline-none cursor-pointer">
+                <option value="admin">admin (Unassign)</option>
+                {% for u in all_usernames %}
+                {% if u != 'admin' %}<option value="{{ u }}">{{ u }}</option>{% endif %}
+                {% endfor %}
+            </select>
+            <div class="flex gap-4 pt-6">
+                <button type="button" onclick="closeAssignModal()" class="flex-1 bg-gray-800 text-gray-400 font-bold py-4 rounded-xl uppercase text-xs">Cancel</button>
+                <button type="submit" class="flex-1 bg-blue-600 text-white font-black py-4 rounded-xl uppercase text-xs shadow-xl">Assign</button>
+            </div>
         </form>
     </div>
 </div>
@@ -154,25 +165,66 @@
         <h3 class="text-2xl font-black text-white mb-8 uppercase border-b border-gray-800 pb-4 text-center">Create New Zone</h3>
         <form action="/zones/add" method="POST" id="addZoneForm" class="space-y-6" onsubmit="handleAjaxForm(event, this, 'dashboard-dynamic-content', restoreDashboardState)">
             <div class="grid grid-cols-2 gap-4">
-                <div><label class="block text-[10px] font-bold text-gray-500 uppercase mb-2 ml-1">Direction</label><select name="direction" id="zoneDirection" onchange="updatePlaceholder()" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-4 text-white font-bold outline-none text-sm cursor-pointer"><option value="forward">Forward</option><option value="reverse">Reverse</option></select></div>
-                <div><label class="block text-[10px] font-bold text-gray-500 uppercase mb-2 ml-1">Type</label><select name="type" id="zoneType" onchange="toggleZoneFields()" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-4 text-white font-bold outline-none text-sm cursor-pointer"><option value="master">Master</option><option value="slave">Slave</option><option value="forward">Forwarder</option><option value="hint">Hint (Root Zone)</option></select></div>
+                <div>
+                    <label class="block text-[10px] font-bold text-gray-500 uppercase mb-2 ml-1">Direction</label>
+                    <select name="direction" id="zoneDirection" onchange="updatePlaceholder()" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-4 text-white font-bold outline-none text-sm cursor-pointer">
+                        <option value="forward">Forward</option>
+                        <option value="reverse">Reverse</option>
+                    </select>
+                </div>
+                <div>
+                    <label class="block text-[10px] font-bold text-gray-500 uppercase mb-2 ml-1">Type</label>
+                    <select name="type" id="zoneType" onchange="toggleZoneFields()" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-4 text-white font-bold outline-none text-sm cursor-pointer">
+                        <option value="master">Master</option>
+                        <option value="slave">Slave</option>
+                        <option value="forward">Forwarder</option>
+                        <option value="hint">Hint (Root Zone)</option>
+                    </select>
+                </div>
             </div>
-            <div id="domainContainer"><label id="domainLabel" class="block text-[10px] font-bold text-gray-500 uppercase mb-2 ml-1">Domain Name</label><input type="text" name="domain" id="domainInput" placeholder="example.com" required class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-4 text-white font-mono outline-none focus:border-blue-500 transition-colors"></div>
+            
+            <div id="domainContainer">
+                <label id="domainLabel" class="block text-[10px] font-bold text-gray-500 uppercase mb-2 ml-1">Domain Name</label>
+                <input type="text" name="domain" id="domainInput" placeholder="example.com" required class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-4 text-white font-mono outline-none focus:border-blue-500 transition-colors">
+            </div>
+            
             <div id="masterFields" class="space-y-4">
-				<label class="block text-[10px] font-bold text-gray-500 uppercase mb-2 ml-1">Template (Required)</label>
-				<select name="template_name" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-4 text-white font-bold outline-none cursor-pointer" required>
-					{% if not templates %}
-						<option value="" disabled selected>No templates assigned to your account!</option>
-					{% else %}
-						{% for tname in templates.keys() %}
-							<option value="{{ tname }}">{{ tname }}</option>
-						{% endfor %}
-					{% endif %}
-				</select>
-			</div>
-            <div id="slaveFields" class="hidden space-y-4"><div class="flex justify-between items-center"><label class="text-[10px] font-bold text-gray-500 uppercase">Master IPs</label><button type="button" onclick="addDynamicIp('slaveIpCont', 'master_ips[]')" class="text-[9px] bg-purple-600/20 text-purple-400 px-3 py-1 rounded font-black uppercase">+ Add</button></div><div id="slaveIpCont" class="space-y-2"><input type="text" name="master_ips[]" placeholder="1.2.3.4" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-3 text-white font-mono"></div></div>
-            <div id="forwardFields" class="hidden space-y-4"><div class="flex justify-between items-center"><label class="text-[10px] font-bold text-gray-500 uppercase">Forwarders</label><button type="button" onclick="addDynamicIp('forwardIpCont', 'forwarders[]')" class="text-[9px] bg-blue-600/20 text-blue-400 px-3 py-1 rounded font-black uppercase">+ Add</button></div><div id="forwardIpCont" class="space-y-2"><input type="text" name="forwarders[]" placeholder="8.8.8.8" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-3 text-white font-mono"></div></div>
-            <div class="flex gap-4 pt-6 border-t border-gray-800"><button type="button" onclick="closeAddModal()" class="flex-1 bg-gray-800 text-gray-400 font-bold py-4 rounded-xl hover:text-white transition uppercase text-xs tracking-widest">Cancel</button><button type="submit" class="flex-1 bg-blue-600 text-white font-black py-4 rounded-xl shadow-xl transition uppercase text-xs tracking-widest hover:bg-blue-500">Create</button></div>
+                <label class="block text-[10px] font-bold text-gray-500 uppercase mb-2 ml-1">Template (Required)</label>
+                <select name="template_name" id="templateSelect" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-4 text-white font-bold outline-none cursor-pointer" required>
+                    {% if not templates %}
+                        <option value="" disabled selected>No templates assigned to your account!</option>
+                    {% else %}
+                        {% for tname in templates.keys() %}
+                            <option value="{{ tname }}">{{ tname }}</option>
+                        {% endfor %}
+                    {% endif %}
+                </select>
+            </div>
+            
+            <div id="slaveFields" class="hidden space-y-4">
+                <div class="flex justify-between items-center">
+                    <label class="text-[10px] font-bold text-gray-500 uppercase">Master IPs</label>
+                    <button type="button" onclick="addDynamicIp('slaveIpCont', 'master_ips[]')" class="text-[9px] bg-purple-600/20 text-purple-400 px-3 py-1 rounded font-black uppercase">+ Add</button>
+                </div>
+                <div id="slaveIpCont" class="space-y-2">
+                    <input type="text" name="master_ips[]" placeholder="1.2.3.4" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-3 text-white font-mono">
+                </div>
+            </div>
+            
+            <div id="forwardFields" class="hidden space-y-4">
+                <div class="flex justify-between items-center">
+                    <label class="text-[10px] font-bold text-gray-500 uppercase">Forwarders</label>
+                    <button type="button" onclick="addDynamicIp('forwardIpCont', 'forwarders[]')" class="text-[9px] bg-blue-600/20 text-blue-400 px-3 py-1 rounded font-black uppercase">+ Add</button>
+                </div>
+                <div id="forwardIpCont" class="space-y-2">
+                    <input type="text" name="forwarders[]" placeholder="8.8.8.8" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-3 text-white font-mono">
+                </div>
+            </div>
+            
+            <div class="flex gap-4 pt-6 border-t border-gray-800">
+                <button type="button" onclick="closeAddModal()" class="flex-1 bg-gray-800 text-gray-400 font-bold py-4 rounded-xl hover:text-white transition uppercase text-xs tracking-widest">Cancel</button>
+                <button type="submit" class="flex-1 bg-blue-600 text-white font-black py-4 rounded-xl shadow-xl transition uppercase text-xs tracking-widest hover:bg-blue-500">Create</button>
+            </div>
         </form>
     </div>
 </div>
@@ -181,8 +233,17 @@
     <div class="bg-[#12161f] border border-gray-800 w-full max-w-md rounded-3xl p-8 shadow-2xl">
         <h3 class="text-2xl font-black text-white mb-8 uppercase text-center italic">System Backup & Restore</h3>
         <div class="space-y-6">
-            <div class="bg-gray-900/50 p-6 rounded-2xl border border-gray-800"><h4 class="text-xs font-black text-green-500 uppercase mb-2">Export Data</h4><a href="/backup/export" class="block w-full text-center bg-green-600/20 text-green-400 font-black py-3 rounded-xl border border-green-500/30 uppercase text-xs">Download ZIP</a></div>
-            <div class="bg-gray-900/50 p-6 rounded-2xl border border-gray-800"><h4 class="text-xs font-black text-blue-500 uppercase mb-2">Restore Data</h4><form action="/backup/import" method="POST" enctype="multipart/form-data" class="space-y-4"><input type="file" name="backup_file" required class="block w-full text-xs text-gray-400 file:bg-gray-800 file:text-white file:border-0 file:py-2 file:px-4 file:rounded-lg"><button type="submit" class="w-full bg-blue-600 text-white font-black py-3 rounded-xl uppercase text-xs shadow-xl">Restore ZIP</button></form></div>
+            <div class="bg-gray-900/50 p-6 rounded-2xl border border-gray-800">
+                <h4 class="text-xs font-black text-green-500 uppercase mb-2">Export Data</h4>
+                <a href="/backup/export" class="block w-full text-center bg-green-600/20 text-green-400 font-black py-3 rounded-xl border border-green-500/30 uppercase text-xs">Download ZIP</a>
+            </div>
+            <div class="bg-gray-900/50 p-6 rounded-2xl border border-gray-800">
+                <h4 class="text-xs font-black text-blue-500 uppercase mb-2">Restore Data</h4>
+                <form action="/backup/import" method="POST" enctype="multipart/form-data" class="space-y-4">
+                    <input type="file" name="backup_file" required class="block w-full text-xs text-gray-400 file:bg-gray-800 file:text-white file:border-0 file:py-2 file:px-4 file:rounded-lg">
+                    <button type="submit" class="w-full bg-blue-600 text-white font-black py-3 rounded-xl uppercase text-xs shadow-xl">Restore ZIP</button>
+                </form>
+            </div>
         </div>
         <button onclick="closeBackupModal()" class="w-full mt-6 bg-gray-800 text-gray-400 py-3 rounded-xl uppercase text-xs font-bold">Close</button>
     </div>
@@ -200,17 +261,21 @@ function renderLiveCharts() {
         const ipsData = times.map(t => todayData.timeline[t].unique_ips);
         const tldLabels = Object.keys(todayData.tlds).sort((a,b) => todayData.tlds[b] - todayData.tlds[a]).slice(0, 5);
         const tldData = tldLabels.map(t => todayData.tlds[t]);
+        
         const chartOptions = { animation: false, interaction: { mode: 'index', intersect: false }, responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { x: { display: false }, y: { grid: { color: '#1f2937' }, beginAtZero: true } } };
+        
         if(!rpsChartInst) {
             rpsChartInst = new Chart(document.getElementById('rpsChart'), { type: 'line', data: { labels: times, datasets: [{ data: rpsData, borderColor: '#3b82f6', fill: true, tension: 0.4 }] }, options: chartOptions });
         } else {
             rpsChartInst.data.labels = times; rpsChartInst.data.datasets[0].data = rpsData; rpsChartInst.update('none');
         }
+        
         if(!ipsChartInst) {
             ipsChartInst = new Chart(document.getElementById('ipsChart'), { type: 'bar', data: { labels: times, datasets: [{ data: ipsData, backgroundColor: '#8b5cf6' }] }, options: chartOptions });
         } else {
             ipsChartInst.data.labels = times; ipsChartInst.data.datasets[0].data = ipsData; ipsChartInst.update('none');
         }
+        
         if(!tldChartInst) {
             tldChartInst = new Chart(document.getElementById('tldChart'), { type: 'doughnut', data: { labels: tldLabels, datasets: [{ data: tldData, backgroundColor: ['#3b82f6', '#10b981', '#f59e0b', '#8b5cf6', '#6366f1'], borderWidth: 0 }] }, options: { animation: false, responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { color: '#9ca3af', font: { size: 10 } } } }, cutout: '70%' } });
         } else {
@@ -238,7 +303,8 @@ async function handleAjaxForm(e, form, containerId, callback) {
 }
 
 async function handleAjaxLink(e, link, containerId, callback, confirmMsg) {
-    e.preventDefault(); if(confirmMsg && !confirm(confirmMsg)) return;
+    e.preventDefault(); 
+    if(confirmMsg && !confirm(confirmMsg)) return;
     try {
         const res = await fetch(link.href);
         const html = await res.text();
@@ -247,7 +313,7 @@ async function handleAjaxLink(e, link, containerId, callback, confirmMsg) {
         if(newCont) {
             document.getElementById(containerId).innerHTML = newCont.innerHTML;
             if(callback) callback();
-            AppToast.fire({ icon: 'success', title: 'Deleted' });
+            AppToast.fire({ icon: 'success', title: 'Action Completed' });
         } else window.location.reload();
     } catch(err) { window.location.reload(); }
 }
@@ -255,16 +321,36 @@ async function handleAjaxLink(e, link, containerId, callback, confirmMsg) {
 // DASHBOARD LOGIC
 function openBackupModal() { document.getElementById('backupModal').classList.remove('hidden'); }
 function closeBackupModal() { document.getElementById('backupModal').classList.add('hidden'); }
-function openAddModal() { document.getElementById('addZoneModal').classList.remove('hidden'); }
-function closeAddModal() { document.getElementById('addZoneModal').classList.add('hidden'); }
-function openAssignModal(d, o) { document.getElementById('assignDomainInput').value=d; document.getElementById('assignDomainDisplay').innerText=d; document.getElementById('assignOwnerSelect').value=o; document.getElementById('assignZoneModal').classList.remove('hidden'); }
-function closeAssignModal() { document.getElementById('assignZoneModal').classList.add('hidden'); }
+
+function openAddModal() { 
+    document.getElementById('addZoneForm').reset();
+    document.getElementById('addZoneModal').classList.remove('hidden');
+    toggleZoneFields();
+}
+
+function closeAddModal() { 
+    // Καθαρισμός όλων των δυναμικών IPs κατά το κλείσιμο
+    document.getElementById('slaveIpCont').innerHTML = '<input type="text" name="master_ips[]" placeholder="1.2.3.4" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-3 text-white font-mono">';
+    document.getElementById('forwardIpCont').innerHTML = '<input type="text" name="forwarders[]" placeholder="8.8.8.8" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-3 text-white font-mono">';
+    document.getElementById('addZoneModal').classList.add('hidden'); 
+}
+
+function openAssignModal(d, o) { 
+    document.getElementById('assignDomainInput').value=d; 
+    document.getElementById('assignDomainDisplay').innerText=d; 
+    document.getElementById('assignOwnerSelect').value=o; 
+    document.getElementById('assignZoneModal').classList.remove('hidden'); 
+}
+
+function closeAssignModal() { 
+    document.getElementById('assignZoneModal').classList.add('hidden'); 
+}
 
 function toggleZoneFields() { 
     const type = document.getElementById('zoneType').value;
     const domCont = document.getElementById('domainContainer');
     const domInp = document.getElementById('domainInput');
-    const tmplSelect = document.querySelector('select[name="template_name"]');
+    const tmplSelect = document.getElementById('templateSelect');
     
     if(type === 'hint') { 
         domCont.classList.add('hidden'); 
@@ -274,7 +360,7 @@ function toggleZoneFields() {
         if(domInp.value === '.') domInp.value = ''; 
     }
     
-    // Εμφάνιση/Απόκρυψη και προσθήκη/αφαίρεση του Required!
+    // Ασφαλής έλεγχος με το νέο ID
     if (type === 'master') {
         document.getElementById('masterFields').style.display = 'block';
         if (tmplSelect) tmplSelect.required = true;
@@ -295,7 +381,7 @@ function updatePlaceholder() {
 
 function addDynamicIp(contId, name) { 
     const div = document.createElement('div'); div.className = 'flex gap-2 mt-2'; 
-    div.innerHTML = `<input type="text" name="${name}" placeholder="0.0.0.0" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-3 text-white font-mono"><button type="button" onclick="this.parentElement.remove()" class="text-red-500 font-black px-4">×</button>`; 
+    div.innerHTML = `<input type="text" name="${name}" placeholder="0.0.0.0" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-3 text-white font-mono"><button type="button" onclick="this.parentElement.remove()" class="text-red-500 font-black px-4 hover:text-red-400 transition">×</button>`; 
     document.getElementById(contId).appendChild(div); 
 }
 
@@ -318,35 +404,52 @@ function switchTab(tab) {
 }
 
 function filterZones() { 
-    let input = document.getElementById('zoneSearch').value.toLowerCase(), items = document.querySelectorAll('.zone-item'), count = 0; 
+    let input = document.getElementById('zoneSearch').value.toLowerCase();
+    let items = document.querySelectorAll('.zone-item');
+    let count = 0; 
     items.forEach(item => { 
-        let domain = item.getAttribute('data-domain'), owner = item.getAttribute('data-owner') || '', category = item.getAttribute('data-category'), match = (domain.includes(input) || owner.includes(input)) && category === activeTab; 
-        item.classList.toggle('hidden', !match); if(match) count++; 
+        let domain = item.getAttribute('data-domain') || '';
+        let owner = item.getAttribute('data-owner') || '';
+        let category = item.getAttribute('data-category');
+        let match = (domain.includes(input) || owner.includes(input)) && category === activeTab; 
+        item.classList.toggle('hidden', !match); 
+        if(match) count++; 
     }); 
-    const msg = document.getElementById('noZonesMsg'); if(msg) msg.classList.toggle('hidden', count > 0); 
+    const msg = document.getElementById('noZonesMsg'); 
+    if(msg) msg.classList.toggle('hidden', count > 0); 
 }
 
 function updateLiveStatus() {
     fetch('/api/service/status').then(r => r.json()).then(data => {
         const dot = document.getElementById('status-dot'), text = document.getElementById('status-text');
-        if(data.status === 'active') {
-            dot.className = "w-2.5 h-2.5 rounded-full bg-green-500 shadow-[0_0_10px_rgba(34,197,94,0.8)] animate-pulse";
-            text.innerHTML = `Service: Active <span class="text-gray-600 ml-2 font-normal italic lowercase">(since ${data.uptime})</span>`;
-            text.classList.replace('text-gray-500', 'text-green-400');
-        } else {
-            dot.className = "w-2.5 h-2.5 rounded-full bg-red-500 shadow-[0_0_10px_rgba(239,68,68,0.8)]";
-            text.innerText = "Service: Offline";
-            text.classList.replace('text-green-400', 'text-red-500');
+        if(dot && text) {
+            if(data.status === 'active') {
+                dot.className = "w-2.5 h-2.5 rounded-full bg-green-500 shadow-[0_0_10px_rgba(34,197,94,0.8)] animate-pulse";
+                text.innerHTML = `Service: Active <span class="text-gray-600 ml-2 font-normal italic lowercase">(since ${data.uptime})</span>`;
+                text.classList.replace('text-gray-500', 'text-green-400');
+                text.classList.replace('text-red-500', 'text-green-400'); // Ασφαλής αλλαγή
+            } else {
+                dot.className = "w-2.5 h-2.5 rounded-full bg-red-500 shadow-[0_0_10px_rgba(239,68,68,0.8)]";
+                text.innerText = "Service: Offline";
+                text.classList.replace('text-green-400', 'text-red-500');
+                text.classList.replace('text-gray-500', 'text-red-500');
+            }
         }
-    });
+    }).catch(e => console.log('Status check error:', e)); // Προστέθηκε catch
 }
 
 function restoreDashboardState() { switchTab(activeTab); updateLiveStatus(); }
 
 document.addEventListener("DOMContentLoaded", () => {
-    renderLiveCharts();
-    setInterval(renderLiveCharts, 10000);
-    setInterval(updateLiveStatus, 5000);
+    // Μόνο οι Admin βλέπουν τα charts
+    if(document.getElementById('rpsChart')) {
+        renderLiveCharts();
+        setInterval(renderLiveCharts, 10000);
+    }
+    // Ο έλεγχος του service τρέχει μόνο αν υπάρχει το dot
+    if(document.getElementById('status-dot')) {
+        setInterval(updateLiveStatus, 5000);
+    }
     restoreDashboardState();
 });
 </script>

+ 42 - 18
templates/manage_zone.html

@@ -20,16 +20,16 @@
                 <span class="text-[10px] bg-blue-900/30 text-blue-400 px-3 py-1 rounded-full uppercase font-black border border-blue-800/50">{{ type }}</span>
                 
                 {% if type == 'slave' %}
-					<button onclick="runDigTest('{{ domain }}')" class="text-[10px] bg-green-600/20 text-green-400 px-3 py-1 rounded-full uppercase font-black border border-green-500/30 hover:bg-green-600 hover:text-white transition">
-						<i class="fa-solid fa-terminal mr-1"></i> Test Transfer (Dig)
-					</button>
-				{% endif %}
+                    <button onclick="runDigTest('{{ domain }}')" class="text-[10px] bg-green-600/20 text-green-400 px-3 py-1 rounded-full uppercase font-black border border-green-500/30 hover:bg-green-600 hover:text-white transition">
+                        <i class="fa-solid fa-terminal mr-1"></i> Test Transfer (Dig)
+                    </button>
+                {% endif %}
 
                 {% if type == 'slave' %}
                     <div class="flex items-center gap-2 border-l border-gray-800 pl-3">
                         <span class="text-[10px] text-purple-400 font-bold uppercase tracking-widest">Master DNS: {{ master_ips }}</span>
                         <button type="button" onclick="openEditIpsModal('{{ master_ips }}')" class="text-[9px] bg-gray-800 hover:bg-gray-700 px-2 py-1 rounded-lg text-gray-300 font-bold uppercase transition">Edit IPs</button>
-                        <a href="/zones/manage/{{ 'root_zone' if domain == '.' else domain }}/convert_to_master" onclick="handleAjaxLink(event, this, 'zone-dynamic-content', restoreRecordTab, 'Convert to Master?')" class="text-[9px] bg-yellow-600/20 text-yellow-500 px-2 py-1 rounded-lg font-black uppercase transition border border-yellow-500/30">Convert to Master</a>
+                        <a href="/zones/manage/{{ 'root_zone' if domain == '.' else domain|urlencode }}/convert_to_master" onclick="handleAjaxLink(event, this, 'zone-dynamic-content', restoreRecordTab, 'Convert to Master?')" class="text-[9px] bg-yellow-600/20 text-yellow-500 px-2 py-1 rounded-lg font-black uppercase transition border border-yellow-500/30">Convert to Master</a>
                     </div>
                 {% elif type == 'master' %}
                     <div class="flex items-center gap-2 border-l border-gray-800 pl-3">
@@ -57,7 +57,7 @@
         {% if type == 'master' %}
         <div class="bg-[#12161f] p-8 rounded-2xl border border-gray-800 mb-8 shadow-2xl">
             <h3 class="font-black mb-6 text-green-500 uppercase text-xs tracking-widest">Add New Resource Record</h3>
-            <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain }}/add_record" method="POST" class="flex flex-col lg:flex-row gap-4 items-start" onsubmit="handleAjaxForm(event, this, 'zone-dynamic-content', () => { restoreRecordTab(); })">
+            <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain|urlencode }}/add_record" method="POST" class="flex flex-col lg:flex-row gap-4 items-start" onsubmit="handleAjaxForm(event, this, 'zone-dynamic-content', () => { restoreRecordTab(); })">
                 <div class="w-full lg:w-1/4">
                     <input type="text" name="name" id="addName" required placeholder="{% if is_reverse %}e.g. {{ base_ip }}.10{% else %}Name (@){% endif %}" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl px-4 h-[46px] text-white font-mono outline-none focus:border-blue-500 text-sm transition">
                     <p class="text-[9px] text-gray-500 font-black tracking-widest uppercase mt-2 h-3">{% if is_reverse %}IP ADDRESS (e.g. {{ base_ip }}.x){% else %}RECORD NAME{% endif %}</p>
@@ -83,7 +83,7 @@
         <div class="bg-[#12161f] p-6 rounded-2xl border border-gray-800 mb-8 shadow-xl">
             <div class="flex justify-between items-center mb-4 border-b border-gray-800 pb-4">
                 <h3 class="font-black text-purple-400 uppercase text-xs tracking-widest">Live Sync Logs</h3>
-                <a href="/zones/manage/{{ 'root_zone' if domain == '.' else domain }}/sync" onclick="handleAjaxLink(event, this, 'zone-dynamic-content', restoreRecordTab, null)" class="text-[10px] bg-blue-600 hover:bg-blue-500 px-4 py-2 rounded-lg text-white font-black uppercase transition flex items-center gap-2">
+                <a href="/zones/manage/{{ 'root_zone' if domain == '.' else domain|urlencode }}/sync" onclick="handleAjaxLink(event, this, 'zone-dynamic-content', restoreRecordTab, null)" class="text-[10px] bg-blue-600 hover:bg-blue-500 px-4 py-2 rounded-lg text-white font-black uppercase transition flex items-center gap-2">
                     <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path></svg>
                     Sync Now
                 </a>
@@ -120,13 +120,27 @@
                             <tbody class="divide-y divide-gray-800/50">
                                 {% for r in rlist %}
                                 <tr class="hover:bg-white/[0.02] group transition-colors cursor-pointer no-select" 
-                                    {% if type == 'master' %} ondblclick="openEditModal('{{r.raw|e}}', '{{r.friendly_name|e|default(r.name|e)}}', '{{rtype}}', '{{r.value|e|replace("'", "\\'")|replace("\"", "&quot;")}}')" {% endif %}>
+                                    {% if type == 'master' %} 
+                                        data-raw="{{ r.raw|e }}" 
+                                        data-name="{{ r.friendly_name|default(r.name)|e }}" 
+                                        data-type="{{ rtype }}" 
+                                        data-val="{{ r.value|e }}"
+                                        ondblclick="triggerEditModal(this)"
+                                    {% endif %}>
                                     <td class="px-6 py-4 text-blue-400 font-bold truncate max-w-[200px]" title="{{ r.name }}">{{ r.friendly_name | default(r.name) }}</td>
                                     <td class="px-6 py-4 text-gray-400 text-xs break-all">{{ r.value }}</td>
                                     <td class="px-6 py-4 text-right opacity-0 group-hover:opacity-100 transition whitespace-nowrap">
                                         {% if type == 'master' %}
-                                        <button onclick="openEditModal('{{r.raw|e}}', '{{r.friendly_name|e|default(r.name|e)}}', '{{rtype}}', '{{r.value|e|replace("'", "\\'")|replace("\"", "&quot;")}}')" class="text-blue-500 hover:text-blue-400 text-[10px] font-black uppercase mr-4 transition">Edit</button>
-                                        <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain }}/delete_record" method="POST" class="inline" onsubmit="if(!confirm('Delete this record?')) return false; handleAjaxForm(event, this, 'zone-dynamic-content', restoreRecordTab)"><input type="hidden" name="raw_line" value="{{ r.raw }}"><button type="submit" class="text-red-500/50 hover:text-red-500 text-[10px] font-black uppercase transition">Delete</button></form>
+                                        <button type="button" 
+                                                data-raw="{{ r.raw|e }}" 
+                                                data-name="{{ r.friendly_name|default(r.name)|e }}" 
+                                                data-type="{{ rtype }}" 
+                                                data-val="{{ r.value|e }}"
+                                                onclick="triggerEditModal(this)" 
+                                                class="text-blue-500 hover:text-blue-400 text-[10px] font-black uppercase mr-4 transition">
+                                            Edit
+                                        </button>
+                                        <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain|urlencode }}/delete_record" method="POST" class="inline" onsubmit="if(!confirm('Delete this record?')) return false; handleAjaxForm(event, this, 'zone-dynamic-content', restoreRecordTab)"><input type="hidden" name="raw_line" value="{{ r.raw|e }}"><button type="submit" class="text-red-500/50 hover:text-red-500 text-[10px] font-black uppercase transition">Delete</button></form>
                                         {% else %}
                                         <span class="text-gray-600 text-[9px] uppercase font-black tracking-widest italic">Read Only</span>
                                         {% endif %}
@@ -143,7 +157,7 @@
                 {% endif %}
             {% else %}
                 <div class="flex flex-col items-center justify-center p-16 text-center border-2 border-dashed border-gray-800 rounded-2xl">
-                    <svg class="w-12 h-12 text-gray-700 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path></svg>
+                    <svg class="w-12 h-12 text-gray-700 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707-.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"></path></svg>
                     <p class="text-gray-500 font-bold uppercase tracking-widest text-[11px]">No records found yet.</p>
                 </div>
             {% endif %}
@@ -178,7 +192,7 @@
                                 <span class="text-white block font-bold text-sm">{{ b.time }}</span>
                                 <span class="text-gray-600 text-[9px]">{{ b.file }}</span>
                             </div>
-                            <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain }}/rollback/{{ b.ts }}" method="POST" onsubmit="if(!confirm('DANGER! This will overwrite the current zone file with this backup. Continue?')) return false; handleAjaxForm(event, this, 'zone-dynamic-content', restoreRecordTab)">
+                            <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain|urlencode }}/rollback/{{ b.ts }}" method="POST" onsubmit="if(!confirm('DANGER! This will overwrite the current zone file with this backup. Continue?')) return false; handleAjaxForm(event, this, 'zone-dynamic-content', restoreRecordTab)">
                                 <button type="submit" class="bg-yellow-600/20 text-yellow-500 hover:bg-yellow-500 hover:text-white transition px-4 py-2 rounded-lg font-black uppercase text-[10px]">Rollback</button>
                             </form>
                         </div>
@@ -206,7 +220,7 @@
                     <h3 class="text-2xl font-black text-gray-500 uppercase tracking-widest mb-2">Not Protected</h3>
                 {% endif %}
                 
-                <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain }}/dnssec" method="POST" onsubmit="handleAjaxForm(event, this, 'zone-dynamic-content', restoreRecordTab)">
+                <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain|urlencode }}/dnssec" method="POST" onsubmit="handleAjaxForm(event, this, 'zone-dynamic-content', restoreRecordTab)">
                     {% if dnssec_enabled %}
                         <button type="submit" class="bg-red-600/20 text-red-500 hover:bg-red-500 hover:text-white px-8 py-3 rounded-xl font-black uppercase transition text-xs border border-red-500/30">Disable DNSSEC</button>
                     {% else %}
@@ -234,7 +248,7 @@
     <div id="editModal" class="hidden fixed inset-0 bg-black/95 items-center justify-center z-50 p-6 transition-opacity">
         <div class="bg-[#12161f] p-8 rounded-3xl border border-gray-800 shadow-2xl w-full max-w-xl">
             <h3 class="font-black text-xl mb-8 text-blue-400 uppercase border-b border-gray-800 pb-4">Modify Record</h3>
-            <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain }}/edit_record" method="POST" class="space-y-6" onsubmit="handleAjaxForm(event, this, 'zone-dynamic-content', () => { restoreRecordTab(); closeEditModal(); })">
+            <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain|urlencode }}/edit_record" method="POST" class="space-y-6" onsubmit="handleAjaxForm(event, this, 'zone-dynamic-content', () => { restoreRecordTab(); closeEditModal(); })">
                 <input type="hidden" name="old_raw" id="editOldRaw">
                 <div class="grid grid-cols-3 gap-6">
                     <div class="col-span-2">
@@ -262,7 +276,7 @@
     <div id="editIpsModal" class="hidden fixed inset-0 bg-black/95 backdrop-blur-md items-center justify-center z-50 p-6 transition-opacity">
         <div class="bg-[#12161f] border border-gray-800 w-full max-w-md rounded-3xl p-8 shadow-2xl">
             <h3 class="text-xl font-black text-white mb-6 uppercase">Edit DNS Target IPs</h3>
-            <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain }}/edit_ips" method="POST" class="space-y-4" onsubmit="handleAjaxForm(event, this, 'zone-dynamic-content', restoreRecordTab)">
+            <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain|urlencode }}/edit_ips" method="POST" class="space-y-4" onsubmit="handleAjaxForm(event, this, 'zone-dynamic-content', restoreRecordTab)">
                 <div id="editIpsContainer" class="space-y-2"></div>
                 <button type="button" onclick="addEditIpField('')" class="w-full py-3 border-2 border-dashed border-gray-800 rounded-xl text-gray-500 text-[11px] font-black uppercase hover:border-gray-600 hover:text-gray-300 transition">+ Add New IP</button>
                 <div class="flex gap-4 pt-6">
@@ -276,7 +290,7 @@
     <div id="convertToSlaveModal" class="hidden fixed inset-0 bg-black/95 backdrop-blur-md items-center justify-center z-50 p-6 transition-opacity">
         <div class="bg-[#12161f] border border-gray-800 w-full max-w-md rounded-3xl p-8 shadow-2xl">
             <h3 class="text-xl font-black text-purple-400 mb-6 uppercase border-b border-gray-800 pb-4">Convert to Slave</h3>
-            <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain }}/convert_to_slave" method="POST" class="space-y-4" onsubmit="handleAjaxForm(event, this, 'zone-dynamic-content', restoreRecordTab)">
+            <form action="/zones/manage/{{ 'root_zone' if domain == '.' else domain|urlencode }}/convert_to_slave" method="POST" class="space-y-4" onsubmit="handleAjaxForm(event, this, 'zone-dynamic-content', restoreRecordTab)">
                 <p class="text-xs text-gray-400 font-bold mb-4">Please provide the Master DNS server IPs for this zone.</p>
                 <div id="slaveIpsContainer" class="space-y-2">
                     <div class="flex gap-2">
@@ -456,7 +470,9 @@ function switchMainTab(tab) {
         btn.classList.add('border-transparent', 'text-gray-500');
     });
     
-    document.getElementById('mainSection_' + tab).classList.remove('hidden');
+    const targetSection = document.getElementById('mainSection_' + tab);
+    if(targetSection) targetSection.classList.remove('hidden');
+    
     const btn = document.getElementById('mainTab_' + tab);
     if(btn) {
         btn.classList.replace('border-transparent', 'border-blue-500');
@@ -492,11 +508,18 @@ function restoreRecordTab() {
     initTableSort(document.getElementById('zone-dynamic-content'));
 }
 
-function openEditModal(raw, name, type, val) {
+// ΔΙΟΡΘΩΣΗ: Ασφαλής ανάγνωση των data-attributes χωρίς errors από εισαγωγικά!
+function triggerEditModal(el) {
+    const raw = el.getAttribute('data-raw');
+    const name = el.getAttribute('data-name');
+    const type = el.getAttribute('data-type');
+    const val = el.getAttribute('data-val');
+    
     document.getElementById('editOldRaw').value = raw;
     document.getElementById('editName').value = name;
     document.getElementById('editRtype').value = type;
     document.getElementById('editValue').value = val;
+    
     document.getElementById('editModal').classList.replace('hidden', 'flex');
     updateValidation('edit');
     setTimeout(() => {
@@ -505,6 +528,7 @@ function openEditModal(raw, name, type, val) {
         input.setSelectionRange(input.value.length, input.value.length);
     }, 50);
 }
+
 function closeEditModal() { document.getElementById('editModal').classList.replace('flex', 'hidden'); }
 
 function openEditIpsModal(currentIps) {