Forráskód Böngészése

API & User Cluster Sync

root 6 hónapja
szülő
commit
68fd98a133

+ 226 - 24
app.py

@@ -1,7 +1,7 @@
 from flask import Flask, render_template, request, redirect, flash, url_for, jsonify, send_file, session
 from werkzeug.security import check_password_hash, generate_password_hash
 from functools import wraps
-import subprocess, os, re, time, json, zipfile, io, datetime, urllib.request
+import subprocess, os, re, time, json, zipfile, io, datetime, urllib.request, secrets
 
 import config
 from core import file_manager as fm
@@ -40,6 +40,23 @@ def api_required(f):
         return f(*args, **kwargs)
     return dec
 
+# --- USER API AUTH DECORATOR ---
+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 '):
+            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)
+        return jsonify({"status": "error", "message": "Invalid or missing API Token"}), 401
+    return dec
+
 def check_zone_access(domain):
     if session.get('role') == 'admin': return True
     return fm.load_panel_settings().get('zone_owners', {}).get(domain) == session.get('username')
@@ -61,6 +78,13 @@ def logout():
     session.clear()
     return redirect(url_for('login'))
 
+@app.route('/profile')
+@login_required
+def profile():
+    users = fm.load_users()
+    user_data = users.get(session['username'], {})
+    return render_template('profile.html', user_data=user_data)
+
 # --- MAIN DASHBOARD & ZONES ---
 @app.route('/')
 @login_required
@@ -70,18 +94,25 @@ def dashboard():
     zones_raw = [z for z in zones_raw if z[0] != "rpz.blacklist"]
     
     settings = fm.load_panel_settings()
-    owners, users = settings.get('zone_owners', {}), fm.load_users()
+    all_templates = settings.get('templates', {})
+    owners = settings.get('zone_owners', {})
+    users = fm.load_users()
+    current_user_data = users.get(session['username'], {})
     
+    # Φιλτράρισμα Ζωνών & Templates
     if session['role'] == 'admin': 
         zones = zones_raw
+        available_templates = all_templates
     else: 
         zones = [z for z in zones_raw if owners.get(z[0]) == session['username']]
+        assigned_keys = current_user_data.get('assigned_templates', [])
+        available_templates = {k: v for k, v in all_templates.items() if k in assigned_keys}
         
     try: status = subprocess.check_output(["systemctl", "is-active", config.BIND_SERVICE]).decode().strip()
     except: status = "inactive"
     
     view_mode = users.get(session['username'], {}).get('view_mode', 'cards')
-    return render_template('dashboard.html', zones=zones, status=status, templates=settings['templates'], 
+    return render_template('dashboard.html', zones=zones, status=status, templates=available_templates, 
                            owners=owners, all_usernames=list(users.keys()), view_mode=view_mode)
 
 @app.route('/zones/add', methods=['POST'])
@@ -116,14 +147,26 @@ def add_zone():
         
     elif z_type == "master":
         path = os.path.join(config.BIND_PATH, f"db.{domain}")
-        conf += f'\nzone "{domain}" {{\n    type master;\n    file "{path}";\n}};\n'
         t_name = request.form.get('template_name')
-        if t_name:
-            t = fm.load_panel_settings()['templates'].get(t_name)
-            if t: fm.write_file(path, dns.apply_template(t['content'], domain, t))
-        else:
-            s_ip = fm.load_panel_settings().get('server_public_ip', '') or '127.0.0.1'
-            fm.write_file(path, f"$TTL 86400\n@ IN SOA ns1.{domain}. admin.{domain}. (\n {time.strftime('%Y%m%d01')}\n 3600\n 1800\n 604800\n 86400\n)\n@ IN NS ns1.{domain}.\nns1 IN A {s_ip}\n")
+        
+        # MANDATORY TEMPLATE CHECK
+        if not t_name:
+            flash("Template is mandatory for Master zones!", "error")
+            return redirect(url_for('dashboard'))
+            
+        if session['role'] != 'admin':
+            assigned = fm.load_users().get(session['username'], {}).get('assigned_templates', [])
+            if t_name not in assigned: 
+                flash("Access denied to this template!", "error")
+                return redirect(url_for('dashboard'))
+
+        t = fm.load_panel_settings()['templates'].get(t_name)
+        if not t:
+            flash("Template not found!", "error")
+            return redirect(url_for('dashboard'))
+            
+        conf += f'\nzone "{domain}" {{\n    type master;\n    file "{path}";\n}};\n'
+        fm.write_file(path, dns.apply_template(t['content'], domain, t))
             
     elif z_type == "forward":
         ips = "".join([f" {x.strip()};" for x in request.form.getlist('forwarders[]') if x.strip()])
@@ -303,7 +346,6 @@ def toggle_dnssec(domain):
     conf = fm.read_file(config.LOCAL_CONF)
     block = dns.get_zone_block(conf, clean)
     
-    # BUG FIX: Αν δεν βρει τη ζώνη, να μην κρασάρει, αλλά να γυρίσει πίσω
     if not block:
         flash("Zone configuration block not found!", "error")
         return redirect(url_for('manage_zone', domain=domain))
@@ -569,11 +611,16 @@ def save_user():
     users = fm.load_users()
     if action == 'add':
         if u in users: flash("User exists", "error")
-        else: users[u] = {"password": generate_password_hash(p), "role": r, "assigned_templates": assigned, "view_mode": "cards"}
+        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
     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')}
-        if u != old_u: users[u] = users.pop(old_u)
+        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
+        
     fm.save_users(users)
     flash("User saved", "success")
     return redirect(url_for('manage_users'))
@@ -585,6 +632,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
         flash("User deleted", "success")
     return redirect(url_for('manage_users'))
 
@@ -911,7 +959,6 @@ def api_add_slave():
     conf = fm.read_file(config.LOCAL_CONF)
     
     # --- OVERWRITE LOGIC ---
-    # Αν η ζώνη υπάρχει, βρίσκουμε το παλιό block και το αφαιρούμε
     old_block = dns.get_zone_block(conf, domain)
     if old_block:
         conf = conf.replace(old_block, "")
@@ -923,7 +970,6 @@ def api_add_slave():
     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'
     
-    # Γράφουμε το καθαρό config (χωρίς το παλιό block) + τη νέα ζώνη
     fm.write_file(config.LOCAL_CONF, (conf.strip() + "\n" + new_zone).strip() + "\n")
     
     subprocess.run(["rndc", "reconfig"])
@@ -949,7 +995,6 @@ def api_delete_zone(domain):
 @app.route('/api/v1/cluster/ping', methods=['GET'])
 @api_required
 def api_ping():
-    # ΑΦΑΙΡΕΘΗΚΕ ΤΟ AUTO-LINKING! Το Ping πλέον απλά επιβεβαιώνει ότι το API Key είναι σωστό.
     return jsonify({"status": "success", "message": "Node is synchronized"})
 
 @app.route('/cluster/check_node', methods=['POST'])
@@ -974,17 +1019,43 @@ def check_cluster_node():
         print(f"\n[CLUSTER NETWORK ERROR] Node: {node_name} -> Αιτία: {str(e)}\n")
         
     return jsonify({"status": "offline"})
+
+@app.route('/api/v1/cluster/user', methods=['POST'])
+@api_required
+def api_cluster_save_user():
+    data = request.get_json(force=True, silent=True) or {}
+    username, user_data, old_username = data.get('username'), data.get('user_data'), data.get('old_username')
+    
+    if not username or not user_data:
+        return jsonify({"status": "error", "message": "Missing user data"}), 400
+        
+    users = fm.load_users()
+    if old_username and old_username in users and old_username != username:
+        del users[old_username]
+        
+    users[username] = user_data
+    fm.save_users(users)
+    return jsonify({"status": "success", "message": f"User {username} synced"})
+
+@app.route('/api/v1/cluster/user/<username>', methods=['DELETE'])
+@api_required
+def api_cluster_delete_user(username):
+    users = fm.load_users()
+    if username in users and username != 'admin':
+        del users[username]
+        fm.save_users(users)
+    return jsonify({"status": "success", "message": f"User {username} deleted"})
+    
+
 # --- REMOTE SERVICE CONTROL (SLAVE RECEIVER) ---
 @app.route('/api/v1/cluster/service', methods=['POST'])
 @api_required
 def api_cluster_service():
-    # Fix: Ασφαλής ανάγνωση του JSON, χωρίς να σκάει
     data = request.get_json(force=True, silent=True) or {}
     action = data.get('action')
     
     if action in ['start', 'stop', 'restart', 'reload']:
         try:
-            # Fix: Πιάνουμε το πραγματικό error του systemctl αν αποτύχει
             res = subprocess.run(["systemctl", action, config.BIND_SERVICE], capture_output=True, text=True)
             if res.returncode == 0:
                 return jsonify({"status": "success", "message": f"Service {action}ed successfully"})
@@ -1038,20 +1109,29 @@ def sync_all_cluster():
     
     ignore = ["rpz.blacklist", ".", "localhost", "127.in-addr.arpa", "0.in-addr.arpa", "255.in-addr.arpa"]
     valid_zones = [z for z in zones if z not in ignore]
-    
-    if not valid_zones:
-        return jsonify({"status": "error", "message": "No zones found to sync."})
 
     def bulk_sync():
         import urllib.request, json, ssl, time
         settings = fm.load_panel_settings()
         nodes = settings.get('cluster_nodes', [])
+        users = fm.load_users()
         
         ctx = ssl.create_default_context()
         ctx.check_hostname = False
         ctx.verify_mode = ssl.CERT_NONE
 
         for node in nodes:
+            # 1. Sync Users First
+            for u_name, u_data in users.items():
+                try:
+                    url = f"{node['url'].rstrip('/')}/api/v1/cluster/user"
+                    payload = {"username": u_name, "user_data": u_data}
+                    req = urllib.request.Request(url, data=json.dumps(payload).encode('utf-8'), headers={"X-API-Key": node['api_key'], "Content-Type": "application/json"}, method='POST')
+                    urllib.request.urlopen(req, timeout=5, context=ctx)
+                except Exception as e:
+                    print(f"Cluster Sync FAIL: {node['name']} -> User {u_name} ({e})")
+            
+            # 2. Sync Zones
             for z in valid_zones:
                 masters = dns.get_zone_masters_list(z)
                 if not masters:
@@ -1069,12 +1149,134 @@ def sync_all_cluster():
                 except Exception as e:
                     print(f"Cluster Sync FAIL: {node['name']} -> {z} ({e})")
                 
-                # ΠΟΛΥ ΣΗΜΑΝΤΙΚΟ: Περιμένουμε μισό δευτερόλεπτο για να προλάβει ο Slave να γράψει το αρχείο!
                 time.sleep(0.5)
 
     import threading
     threading.Thread(target=bulk_sync, daemon=True).start()
-    return jsonify({"status": "success", "message": f"Sync started! Sending {len(valid_zones)} zones in the background. Please wait ~10 seconds."})
+    return jsonify({"status": "success", "message": f"Global sync started! Sending {len(fm.load_users())} users and {len(valid_zones)} zones in the background."})
+    
+    
+# --- USER API & TOKEN ROUTES ---
+@app.route('/settings/generate_token', methods=['POST'])
+@login_required
+def generate_user_token():
+    users = fm.load_users()
+    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
+    flash("New API Token generated successfully!", "success")
+    return redirect(url_for('profile'))
+
+@app.route('/api/v1/user/zone/check/<domain>', methods=['GET'])
+@user_api_required
+def api_user_check_zone(domain):
+    conf = fm.read_file(config.LOCAL_CONF)
+    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():
+    data = request.get_json(force=True, silent=True) or {}
+    domain = data.get('domain', '').strip()
+    t_name = data.get('template_name')
+    z_type = data.get('type', 'master')
+    
+    if not domain: return jsonify({"status": "error", "message": "Domain is required"}), 400
+    
+    conf = fm.read_file(config.LOCAL_CONF)
+    if f'zone "{domain}"' in conf:
+        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
+            
+        if session['role'] != 'admin':
+            assigned = fm.load_users().get(session['username'], {}).get('assigned_templates', [])
+            if t_name not in assigned:
+                return jsonify({"status": "error", "message": "Template access denied"}), 403
+
+        t = fm.load_panel_settings()['templates'].get(t_name)
+        if not t:
+            return jsonify({"status": "error", "message": "Template not found"}), 404
+            
+        path = os.path.join(config.BIND_PATH, f"db.{domain}")
+        new_conf = conf + f'\nzone "{domain}" {{\n    type master;\n    file "{path}";\n}};\n'
+        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)
+    s = fm.load_panel_settings()
+    s['zone_owners'][domain] = session['username']
+    fm.save_panel_settings(s)
+    
+    subprocess.run(["rndc", "reconfig"])
+    if data.get('sync', True): cm.broadcast_to_cluster('add_slave', domain)
+        
+    return jsonify({"status": "success", "message": f"Zone {domain} created"})
+
+@app.route('/api/v1/user/record/add', methods=['POST'])
+@user_api_required
+def api_user_add_record():
+    data = request.get_json(force=True, silent=True) or {}
+    domain = data.get('domain')
+    rtype = data.get('type', 'A').upper()
+    name = data.get('name')
+    value = data.get('value')
+    
+    if not check_zone_access(domain):
+        return jsonify({"status": "error", "message": "Access denied to this zone"}), 403
+        
+    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
+        
+    new_content = dns.increment_serial(content) + f"{clean_name}\tIN\t{rtype}\t{val}\n"
+    is_valid, err = dns.check_zone_syntax(domain, new_content)
+    if not is_valid: return jsonify({"status": "error", "message": f"Invalid syntax: {err}"}), 400
+        
+    fm.write_file(path, new_content)
+    subprocess.run(["rndc", "reload", domain])
+    if data.get('sync', True): cm.broadcast_to_cluster('add_slave', domain)
+        
+    return jsonify({"status": "success", "message": "Record added"})
+
+@app.route('/api/v1/user/sync', methods=['POST'])
+@user_api_required
+def api_user_sync_cluster():
+    data = request.get_json(force=True, silent=True) or {}
+    domain = data.get('domain')
+    if not check_zone_access(domain): return jsonify({"status": "error", "message": "Access denied"}), 403
+    cm.broadcast_to_cluster('add_slave', domain)
+    return jsonify({"status": "success", "message": "Sync command sent"})
+
+@app.route('/api/v1/user/templates', methods=['GET'])
+@user_api_required
+def api_user_get_templates():
+    users = fm.load_users()
+    assigned = users.get(session['username'], {}).get('assigned_templates', [])
+    all_templates = fm.load_panel_settings().get('templates', {})
+    
+    if session['role'] == 'admin':
+        user_templates = list(all_templates.keys())
+    else:
+        user_templates = [t for t in assigned if t in all_templates]
+        
+    return jsonify({"status": "success", "templates": user_templates})
     
 if __name__ == '__main__':
     # Είναι σημαντικό το threaded=True για να μην κάνει "block" το UI στο self-ping

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


+ 30 - 0
core/cluster_manager.py

@@ -71,4 +71,34 @@ def broadcast_adblock_to_cluster(action):
                 print(f"Cluster AdBlock Sync OK: {node['name']} ({action})")
             except Exception as e:
                 print(f"Cluster AdBlock Sync FAIL: {node['name']} -> {e}")
+    threading.Thread(target=task, daemon=True).start()
+    
+def broadcast_user_to_cluster(action, username, user_data=None, old_username=None):
+    def task():
+        import urllib.request, json, ssl
+        from core.file_manager import load_panel_settings
+        settings = load_panel_settings()
+        nodes = settings.get('cluster_nodes', [])
+        
+        ctx = ssl.create_default_context()
+        ctx.check_hostname = False
+        ctx.verify_mode = ssl.CERT_NONE
+        
+        for node in nodes:
+            try:
+                url = f"{node['url'].rstrip('/')}/api/v1/cluster/user"
+                headers = {"X-API-Key": node['api_key'], "Content-Type": "application/json"}
+                
+                if action == 'save':
+                    payload = {"username": username, "user_data": user_data, "old_username": old_username}
+                    req = urllib.request.Request(url, data=json.dumps(payload).encode(), headers=headers, method='POST')
+                elif action == 'delete':
+                    req = urllib.request.Request(f"{url}/{username}", headers=headers, method='DELETE')
+                    
+                urllib.request.urlopen(req, timeout=5, context=ctx)
+                print(f"Cluster Sync OK: {node['name']} (User {action} -> {username})")
+            except Exception as e:
+                print(f"Cluster Sync FAIL: {node['name']} -> User {username} ({e})")
+                
+    import threading
     threading.Thread(target=task, daemon=True).start()

+ 140 - 159
templates/dashboard.html

@@ -34,64 +34,27 @@
 {% if session.get('role') == 'admin' %}
 <div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-10">
     <div class="bg-[#12161f] border border-gray-800 rounded-2xl p-6 shadow-xl relative">
-        <div class="flex justify-between items-center mb-4">
-            <h3 class="text-[10px] font-black text-gray-500 uppercase tracking-widest">Requests Per Second</h3>
+        <div class="flex justify-between items-center mb-4 text-[10px] font-black text-gray-500 uppercase tracking-widest">
+            <h3>Requests Per Second</h3>
             <span class="flex h-2 w-2 relative"><span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-blue-400 opacity-75"></span><span class="relative inline-flex rounded-full h-2 w-2 bg-blue-500"></span></span>
         </div>
         <div class="h-40"><canvas id="rpsChart"></canvas></div>
     </div>
     <div class="bg-[#12161f] border border-gray-800 rounded-2xl p-6 shadow-xl relative">
-        <div class="flex justify-between items-center mb-4">
-            <h3 class="text-[10px] font-black text-gray-500 uppercase tracking-widest">Top TLDs</h3>
+        <div class="flex justify-between items-center mb-4 text-[10px] font-black text-gray-500 uppercase tracking-widest">
+            <h3>Top TLDs</h3>
             <span class="flex h-2 w-2 relative"><span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span><span class="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span></span>
         </div>
         <div class="h-40 flex justify-center"><canvas id="tldChart"></canvas></div>
     </div>
     <div class="bg-[#12161f] border border-gray-800 rounded-2xl p-6 shadow-xl relative">
-        <div class="flex justify-between items-center mb-4">
-            <h3 class="text-[10px] font-black text-gray-500 uppercase tracking-widest">Unique IPs</h3>
+        <div class="flex justify-between items-center mb-4 text-[10px] font-black text-gray-500 uppercase tracking-widest">
+            <h3>Unique IPs</h3>
             <span class="flex h-2 w-2 relative"><span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-purple-400 opacity-75"></span><span class="relative inline-flex rounded-full h-2 w-2 bg-purple-500"></span></span>
         </div>
         <div class="h-40"><canvas id="ipsChart"></canvas></div>
     </div>
 </div>
-
-<script>
-let rpsChartInst = null, ipsChartInst = null, tldChartInst = null;
-
-function renderLiveCharts() {
-    fetch('/api/stats').then(r => r.json()).then(data => {
-        const today = new Date().toISOString().split('T')[0];
-        const todayData = data[today] || { timeline: {}, tlds: {} };
-        const times = Object.keys(todayData.timeline).sort();
-        const rpsData = times.map(t => todayData.timeline[t].rps);
-        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: [{ label: 'RPS', 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: [{ label: 'Unique IPs', 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, interaction: { mode: 'index', intersect: false }, responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', labels: { color: '#9ca3af', font: { size: 10 } } } }, cutout: '70%' } });
-        } else {
-            tldChartInst.data.labels = tldLabels; tldChartInst.data.datasets[0].data = tldData; tldChartInst.update('none');
-        }
-    }).catch(e => console.log('Stats sync error:', e));
-}
-
-document.addEventListener("DOMContentLoaded", () => {
-    renderLiveCharts();
-    setInterval(renderLiveCharts, 10000); 
-});
-</script>
 {% endif %}
 
 <div class="flex border-b border-gray-800 mb-6 gap-6 overflow-x-auto whitespace-nowrap scrollbar-hide">
@@ -119,32 +82,25 @@ document.addEventListener("DOMContentLoaded", () => {
         {% set is_hint = ztype == 'hint' %}
         {% set is_forward = ztype == 'forward' %}
         {% set tab_cat = 'forward_zones' if is_forward else ('master_forward' if is_master and not is_reverse else 'master_reverse' if is_master and is_reverse else 'slave_forward' if not is_master and not is_reverse else 'slave_reverse') %}
-        {% set friendly_name = zone_name %}
-        {% if is_reverse %}
-            {% set rev_parts = zone_name.replace('.in-addr.arpa', '').replace('.ip6.arpa', '').split('.') %}
-            {% set friendly_name = (rev_parts | reverse | join('.')) ~ '.x (Reverse)' %}
-        {% endif %}
+        
         <div class="zone-item zone-card bg-[#12161f] border border-gray-800 rounded-2xl p-6 shadow-xl hover:border-blue-500/40 transition group relative" data-domain="{{ zone_name|lower }}" data-owner="{{ owners.get(zone_name, 'admin')|lower }}" data-category="{{ tab_cat }}">
             <div class="flex justify-between items-start mb-4">
                 <div class="flex gap-2 flex-wrap">
                     <span class="text-[9px] font-black uppercase tracking-widest px-2 py-1 rounded bg-gray-900 {% if is_hint %}text-yellow-400 border border-yellow-500/20{% elif ztype=='master' %}text-green-400 border border-green-500/20{% elif ztype=='forward' %}text-cyan-400 border border-cyan-500/20{% else %}text-blue-400 border border-blue-500/20{% endif %}">{{ ztype }}</span>
-                    {% if is_hint %}<span class="text-[9px] font-black uppercase tracking-widest px-2 py-1 rounded bg-red-900/30 text-red-500">ROOT</span>{% endif %}
                     {% if session.get('role') == 'admin' %}
                     <span class="text-[9px] font-black text-gray-400 bg-gray-800/50 px-2 py-1 rounded">{{ owners.get(zone_name, 'admin') }}</span>
-                    <button onclick="openAssignModal('{{ zone_name }}', '{{ owners.get(zone_name, 'admin') }}')" class="text-[9px] bg-blue-600/20 text-blue-400 px-2 py-1 rounded border border-blue-500/30">Assign</button>
                     {% endif %}
                 </div>
                 <div class="flex gap-3 opacity-0 group-hover:opacity-100 transition items-center">
-                    {% if is_master and not is_hint %}
-                    <form action="/zones/manage/{{ url_param }}/cluster_sync" method="POST" onsubmit="handleAjaxForm(event, this, 'dashboard-dynamic-content', restoreDashboardState)">
-                        <button type="submit" class="text-blue-400 text-[10px] font-black uppercase tracking-widest hover:text-blue-300 transition">Cluster Sync</button>
-                    </form>
+                    {% 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>
                     {% 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>
                 </div>
             </div>
-            <h3 class="text-xl font-bold text-white {% if not is_reverse %}mb-6{% endif %} font-mono truncate" title="{{ zone_name }}">{% if zone_name == '.' %}. (Root Zone){% else %}{{ friendly_name }}{% endif %}</h3>
-            {% if is_reverse %}<p class="text-[9px] text-gray-500 font-mono mb-4 truncate tracking-widest">{{ zone_name }}</p>{% endif %}
+            <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>
         </div>
         {% endfor %}
@@ -159,18 +115,16 @@ document.addEventListener("DOMContentLoaded", () => {
                 {% for zone_name, ztype in zones %}
                 {% set url_param = 'root_zone' if zone_name == '.' else zone_name %}
                 {% set is_reverse = '.arpa' in zone_name %}
-                {% set is_master = ztype == 'master' or ztype == 'hint' %}{% set is_hint = ztype == 'hint' %}{% set is_forward = ztype == 'forward' %}
+                {% set is_master = ztype == 'master' or ztype == 'hint' %}
+                {% set is_forward = ztype == 'forward' %}
                 {% set tab_cat = 'forward_zones' if is_forward else ('master_forward' if is_master and not is_reverse else 'master_reverse' if is_master and is_reverse else 'slave_forward' if not is_master and not is_reverse else 'slave_reverse') %}
-                {% set friendly_name = zone_name %}
-                {% if is_reverse %}{% set rev_parts = zone_name.replace('.in-addr.arpa', '').replace('.ip6.arpa', '').split('.') %}{% set friendly_name = (rev_parts | reverse | join('.')) ~ '.x (Reverse)' %}{% endif %}
                 <tr class="zone-item group" data-domain="{{ zone_name|lower }}" data-owner="{{ owners.get(zone_name, 'admin')|lower }}" data-category="{{ tab_cat }}">
-                    <td class="p-6 font-mono font-bold text-white">{% if zone_name == '.' %}. (Root Zone){% else %}{{ friendly_name }}{% endif %}{% if is_reverse %}<br><span class="text-[9px] text-gray-500 tracking-widest">{{ zone_name }}</span>{% endif %}</td>
+                    <td class="p-6 font-mono font-bold text-white">{% if zone_name == '.' %}. (Root Zone){% else %}{{ zone_name }}{% endif %}</td>
                     <td class="p-6"><span class="text-[9px] font-black uppercase px-2 py-1 rounded bg-gray-900 {% if is_forward %}text-cyan-400{% else %}text-blue-400{% endif %}">{{ ztype }}</span></td>
                     <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 %}
-                            {% if is_master and not is_hint %}<form action="/zones/manage/{{ url_param }}/cluster_sync" method="POST" onsubmit="handleAjaxForm(event, this, 'dashboard-dynamic-content', restoreDashboardState)" class="inline-block opacity-0 group-hover:opacity-100 transition"><button type="submit" class="text-blue-400 text-[10px] font-black uppercase tracking-widest hover:text-blue-300">Cluster Sync</button></form>{% 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>
                         </div>
@@ -180,175 +134,202 @@ document.addEventListener("DOMContentLoaded", () => {
             </tbody>
         </table>
     </div>
-
+    
     <div id="noZonesMsg" class="hidden py-20 text-center border-2 border-dashed border-gray-800 rounded-3xl mt-6"><span class="text-gray-600 font-black uppercase text-xs">No zones found in this category</span></div>
+</div>
 
-    <div id="assignZoneModal" class="hidden fixed inset-0 bg-black/90 flex items-center justify-center z-50 p-4">
-        <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>
-            </form>
-        </div>
+<div id="assignZoneModal" class="hidden fixed inset-0 bg-black/90 flex items-center justify-center z-50 p-4">
+    <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>
+        </form>
     </div>
+</div>
 
-    <div id="addZoneModal" class="hidden fixed inset-0 bg-black/90 flex items-center justify-center z-50 p-4">
-        <div class="bg-[#12161f] border border-gray-800 w-full max-w-lg rounded-3xl p-8 shadow-2xl">
-            <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>
-                <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</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"><option value="">-- Blank (No Template) --</option>{% for tname in templates.keys() %}<option value="{{ tname }}">{{ tname }}</option>{% endfor %}</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 id="addZoneModal" class="hidden fixed inset-0 bg-black/90 flex items-center justify-center z-50 p-4">
+    <div class="bg-[#12161f] border border-gray-800 w-full max-w-lg rounded-3xl p-8 shadow-2xl">
+        <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>
+            <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>
+        </form>
     </div>
+</div>
 
-    <div id="backupModal" class="hidden fixed inset-0 bg-black/90 flex items-center justify-center z-50 p-4">
-        <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>
-            <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 id="backupModal" class="hidden fixed inset-0 bg-black/90 flex items-center justify-center z-50 p-4">
+    <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>
+        <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>
 </div>
 
 <script>
+// CHART LOGIC
+let rpsChartInst = null, ipsChartInst = null, tldChartInst = null;
+function renderLiveCharts() {
+    fetch('/api/stats').then(r => r.json()).then(data => {
+        const today = new Date().toISOString().split('T')[0];
+        const todayData = data[today] || { timeline: {}, tlds: {} };
+        const times = Object.keys(todayData.timeline).sort();
+        const rpsData = times.map(t => todayData.timeline[t].rps);
+        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 {
+            tldChartInst.data.labels = tldLabels; tldChartInst.data.datasets[0].data = tldData; tldChartInst.update('none');
+        }
+    }).catch(e => console.log('Stats sync error:', e));
+}
+
+// AJAX HELPERS
 async function handleAjaxForm(e, form, containerId, callback) {
     e.preventDefault();
     const btn = form.querySelector('button[type="submit"]');
-    if(btn) { btn.innerHTML = spinnerSvg + ' WAIT'; btn.disabled = true; }
+    if(btn) { btn.disabled = true; }
     try {
         const res = await fetch(form.action, { method: form.method || 'POST', body: new FormData(form) });
         const html = await res.text();
         const doc = new DOMParser().parseFromString(html, 'text/html');
-        const scriptContent = Array.from(doc.querySelectorAll('script')).map(s => s.innerHTML).join('\n');
-        const errorMatch = scriptContent.match(/Toast\.fire\(\{\s*icon:\s*'error',\s*title:\s*'([^']+)'/);
-        const successMatch = scriptContent.match(/Toast\.fire\(\{\s*icon:\s*'success',\s*title:\s*'([^']+)'/);
         const newCont = doc.getElementById(containerId);
         if(newCont) {
             document.getElementById(containerId).innerHTML = newCont.innerHTML;
             if(callback) callback();
-            if(errorMatch) AppToast.fire({ icon: 'error', title: errorMatch[1] });
-            else if(successMatch) AppToast.fire({ icon: 'success', title: successMatch[1] });
-            else AppToast.fire({ icon: 'success', title: 'Action Completed!' });
+            AppToast.fire({ icon: 'success', title: 'Success' });
         } else window.location.reload();
     } catch(err) { window.location.reload(); }
 }
+
 async function handleAjaxLink(e, link, containerId, callback, confirmMsg) {
     e.preventDefault(); if(confirmMsg && !confirm(confirmMsg)) return;
     try {
         const res = await fetch(link.href);
         const html = await res.text();
         const doc = new DOMParser().parseFromString(html, 'text/html');
-        const scriptContent = Array.from(doc.querySelectorAll('script')).map(s => s.innerHTML).join('\n');
-        const errorMatch = scriptContent.match(/Toast\.fire\(\{\s*icon:\s*'error',\s*title:\s*'([^']+)'/);
-        const successMatch = scriptContent.match(/Toast\.fire\(\{\s*icon:\s*'success',\s*title:\s*'([^']+)'/);
         const newCont = doc.getElementById(containerId);
         if(newCont) {
             document.getElementById(containerId).innerHTML = newCont.innerHTML;
             if(callback) callback();
-            if(errorMatch) AppToast.fire({ icon: 'error', title: errorMatch[1] });
-            else if(successMatch) AppToast.fire({ icon: 'success', title: successMatch[1] });
-            else AppToast.fire({ icon: 'success', title: 'Action Executed!' });
+            AppToast.fire({ icon: 'success', title: 'Deleted' });
         } else window.location.reload();
     } catch(err) { window.location.reload(); }
 }
-function initTableSort(container) {
-    const getCellValue = (tr, idx) => tr.children[idx].innerText.trim();
-    const comparer = (idx, asc) => (a, b) => ((v1, v2) => v1 !== '' && v2 !== '' && !isNaN(v1) && !isNaN(v2) ? v1 - v2 : v1.localeCompare(v2, undefined, {numeric: true}))(getCellValue(asc ? a : b, idx), getCellValue(asc ? b : a, idx));
-    container.querySelectorAll('table').forEach(table => {
-        if(table.dataset.sortBound) return; table.dataset.sortBound = "true";
-        table.querySelectorAll('th').forEach(th => {
-            if (th.innerText.trim().toLowerCase() === 'actions') return;
-            th.classList.add('cursor-pointer', 'hover:text-blue-400', 'transition-colors', 'select-none', 'group');
-            const originalText = th.innerHTML;
-            if(!originalText.includes('sort-icon')) { th.innerHTML = `<div class="flex items-center gap-2 justify-${th.classList.contains('text-right') ? 'end' : 'start'}">${originalText}<span class="sort-icon opacity-0 group-hover:opacity-50 transition-opacity text-blue-500 text-lg leading-none">↕</span></div>`; }
-            th.addEventListener('click', function() {
-                const tbody = table.querySelector('tbody'); if(!tbody) return;
-                table.querySelectorAll('.sort-icon').forEach(icon => { icon.innerHTML = '↕'; icon.classList.remove('opacity-100', 'text-blue-400'); icon.classList.add('opacity-0', 'text-blue-500'); });
-                this.asc = !this.asc; const icon = this.querySelector('.sort-icon'); icon.innerHTML = this.asc ? '↑' : '↓';
-                icon.classList.remove('opacity-0', 'text-blue-500', 'group-hover:opacity-50'); icon.classList.add('opacity-100', 'text-blue-400');
-                Array.from(tbody.querySelectorAll('tr')).sort(comparer(Array.from(th.parentNode.children).indexOf(th), this.asc)).forEach(tr => tbody.appendChild(tr));
-            });
-        });
-    });
-}
+
+// 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'); setTimeout(() => document.getElementById('domainInput').focus(), 100); }
-function closeAddModal() { const m = document.getElementById('addZoneModal'); if(m) m.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() { const m = document.getElementById('assignZoneModal'); if(m) m.classList.add('hidden'); }
+function closeAssignModal() { document.getElementById('assignZoneModal').classList.add('hidden'); }
+
 function toggleZoneFields() {
     const type = document.getElementById('zoneType').value, domCont = document.getElementById('domainContainer'), domInp = document.getElementById('domainInput');
-    if(type === 'hint') { domCont.classList.add('hidden'); domInp.required = false; domInp.value = '.'; } 
-    else { domCont.classList.remove('hidden'); domInp.required = true; if(domInp.value === '.') domInp.value = ''; }
+    if(type === 'hint') { domCont.classList.add('hidden'); domInp.value = '.'; } 
+    else { domCont.classList.remove('hidden'); if(domInp.value === '.') domInp.value = ''; }
     document.getElementById('masterFields').style.display = (type === 'master') ? 'block' : 'none';
     document.getElementById('slaveFields').style.display = (type === 'slave') ? 'block' : 'none';
     document.getElementById('forwardFields').style.display = (type === 'forward') ? 'block' : 'none';
 }
+
 function updatePlaceholder() { 
     const dir = document.getElementById('zoneDirection').value, inp = document.getElementById('domainInput'), lbl = document.getElementById('domainLabel');
-    if(dir === 'reverse') { lbl.innerText = 'IP SUBNET (e.g. 192.168.1)'; inp.placeholder = '192.168.1 or 10.0'; } 
+    if(dir === 'reverse') { lbl.innerText = 'IP SUBNET (e.g. 192.168.1)'; inp.placeholder = '192.168.1'; } 
     else { lbl.innerText = 'DOMAIN NAME'; inp.placeholder = 'example.com'; }
 }
+
 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 outline-none focus:border-blue-500 transition"><button type="button" onclick="this.parentElement.remove()" class="text-red-500 font-black px-4 hover:bg-red-500/10 rounded-xl transition">×</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">×</button>`; 
     document.getElementById(contId).appendChild(div); 
 }
-function changeViewMode(mode) { document.getElementById('zonesGrid').classList.toggle('hidden', mode==='table'); document.getElementById('zonesTableContainer').classList.toggle('hidden', mode==='cards'); fetch('/settings/view_mode', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({mode: mode}) }); }
+
+function changeViewMode(mode) { 
+    document.getElementById('zonesGrid').classList.toggle('hidden', mode==='table'); 
+    document.getElementById('zonesTableContainer').classList.toggle('hidden', mode==='cards');
+    document.getElementById('btnViewCards').className = mode==='cards' ? 'px-6 rounded-xl text-xs font-black uppercase transition bg-blue-600 text-white shadow-lg' : 'px-6 rounded-xl text-xs font-black uppercase transition text-gray-500';
+    document.getElementById('btnViewTable').className = mode==='table' ? 'px-6 rounded-xl text-xs font-black uppercase transition bg-blue-600 text-white shadow-lg' : 'px-6 rounded-xl text-xs font-black uppercase transition text-gray-500';
+    fetch('/settings/view_mode', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({mode: mode}) }); 
+}
+
 let activeTab = 'master_forward';
 function switchTab(tab) { 
     activeTab = tab; 
     ['master_forward', 'master_reverse', 'slave_forward', 'slave_reverse', 'forward_zones'].forEach(t => { 
-        const btn = document.getElementById('btnTab_' + t); if(btn) btn.className = (t === activeTab) ? "pb-4 text-[10px] font-black uppercase tracking-widest text-blue-500 border-b-2 border-blue-500 transition focus:outline-none" : "pb-4 text-[10px] font-black uppercase tracking-widest text-gray-500 border-b-2 border-transparent hover:text-gray-300 transition focus:outline-none"; 
+        const btn = document.getElementById('btnTab_' + t); 
+        if(btn) btn.className = (t === activeTab) ? "pb-4 text-[10px] font-black uppercase tracking-widest text-blue-500 border-b-2 border-blue-500 transition focus:outline-none" : "pb-4 text-[10px] font-black uppercase tracking-widest text-gray-500 border-b-2 border-transparent hover:text-gray-300 transition focus:outline-none"; 
     }); 
     filterZones(); 
 }
+
 function filterZones() { 
     let input = document.getElementById('zoneSearch').value.toLowerCase(), items = document.querySelectorAll('.zone-item'), 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++; 
     }); 
-    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 restoreDashboardState() { switchTab(activeTab); initTableSort(document.getElementById('dashboard-dynamic-content')); const searchInput = document.getElementById('zoneSearch'); if(searchInput && searchInput.value) filterZones(); updateLiveStatus(); }
 
 function updateLiveStatus() {
-    fetch('/api/service/status')
-        .then(r => r.json())
-        .then(data => {
-            const dot = document.getElementById('status-dot');
-            const 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 / Crashed";
-                text.classList.replace('text-green-400', 'text-red-500');
-            }
-        })
-        .catch(() => {
-            const txt = document.getElementById('status-text');
-            if(txt) txt.innerText = "Status: Connection Error";
-        });
+    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');
+        }
+    });
 }
-setInterval(updateLiveStatus, 5000);
-document.addEventListener("DOMContentLoaded", restoreDashboardState);
+
+function restoreDashboardState() { switchTab(activeTab); updateLiveStatus(); }
+
+document.addEventListener("DOMContentLoaded", () => {
+    renderLiveCharts();
+    setInterval(renderLiveCharts, 10000);
+    setInterval(updateLiveStatus, 5000);
+    restoreDashboardState();
+});
 </script>
 {% endblock %}

+ 4 - 0
templates/layout.html

@@ -58,6 +58,10 @@
                 <svg class="w-5 h-5 group-hover:scale-110 transition-transform" 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"></path></svg>
                 Cluster Nodes
             </a>
+			<a href="/profile" class="flex items-center gap-3 px-4 py-2 text-gray-400 hover:text-white transition">
+			<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>
+			<span class="text-xs font-black uppercase tracking-widest">My API</span>
+		</a>
             {% endif %}
         </nav>
         

+ 147 - 0
templates/profile.html

@@ -0,0 +1,147 @@
+{% extends "layout.html" %}
+{% block content %}
+<div class="max-w-4xl mx-auto p-4 md:p-8">
+    <h2 class="text-3xl font-black text-white tracking-tighter italic mb-8 uppercase flex items-center gap-3">
+        <svg class="w-8 h-8 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"></path></svg>
+        User Profile & API
+    </h2>
+    
+    <div class="bg-[#12161f] border border-gray-800 rounded-3xl p-6 md:p-8 shadow-2xl">
+        <div class="flex items-center gap-6 mb-8 border-b border-gray-800 pb-6">
+            <div class="w-16 h-16 bg-blue-600 rounded-2xl flex items-center justify-center text-2xl font-black text-white shadow-lg">
+                {{ session['username'][0].upper() }}
+            </div>
+            <div>
+                <h3 class="text-xl font-bold text-white">{{ session['username'] }}</h3>
+                <p class="text-gray-500 text-xs font-bold uppercase tracking-widest">{{ session['role'] }} Account</p>
+            </div>
+        </div>
+
+        <div class="mb-8 p-5 bg-blue-900/10 border border-blue-500/20 rounded-2xl">
+            <h4 class="text-[10px] font-black text-blue-400 uppercase tracking-widest mb-3">Your Assigned Templates</h4>
+            <div class="flex gap-2 flex-wrap">
+                {% if user_data.assigned_templates %}
+                    {% for t in user_data.assigned_templates %}
+                        <span class="bg-blue-600/20 text-blue-400 px-3 py-1.5 rounded-lg text-[10px] font-mono border border-blue-500/30">{{ t }}</span>
+                    {% endfor %}
+                {% else %}
+                    <span class="text-gray-500 text-[10px] uppercase font-bold tracking-widest">No templates assigned.</span>
+                {% endif %}
+            </div>
+        </div>
+
+        <div class="space-y-6">
+            <h4 class="text-[10px] font-black text-gray-400 uppercase tracking-widest">Personal API Access Token</h4>
+            <p class="text-gray-500 text-xs">Use this token to authenticate your API requests. Keep it secret!</p>
+            
+            {% if user_data.api_token %}
+            <div class="flex flex-col lg:flex-row gap-4 items-stretch">
+                <code class="flex-1 bg-[#0b0e14] px-6 py-4 rounded-xl border border-gray-800 text-green-400 font-mono text-sm break-all shadow-inner">
+                    {{ user_data.api_token }}
+                </code>
+                <form action="/settings/generate_token" method="POST" class="flex" onsubmit="return confirm('Regenerating will break existing API integrations. Continue?');">
+                    <button type="submit" class="w-full lg:w-auto bg-red-900/20 text-red-400 hover:bg-red-600 hover:text-white px-8 py-4 rounded-xl text-[10px] font-black uppercase tracking-widest border border-red-500/30 transition shadow-lg">
+                        Regenerate
+                    </button>
+                </form>
+            </div>
+            {% else %}
+            <form action="/settings/generate_token" method="POST">
+                <button type="submit" class="bg-blue-600 hover:bg-blue-500 text-white px-8 py-4 rounded-xl text-xs font-black uppercase tracking-widest shadow-xl transition">
+                    Generate My First API Token
+                </button>
+            </form>
+            {% endif %}
+        </div>
+
+        <div class="mt-12 pt-8 border-t border-gray-800">
+            <h4 class="text-[10px] font-black text-gray-400 uppercase tracking-widest mb-6 flex items-center gap-2">
+                <svg class="w-4 h-4 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
+                API Examples (Documentation)
+            </h4>
+
+            <div class="space-y-6">
+                <div class="space-y-2">
+                    <p class="text-[11px] font-bold text-gray-300 uppercase">1. Get Available Profiles (Templates)</p>
+                    <div class="bg-[#0b0e14] p-5 rounded-2xl font-mono text-[11px] text-gray-400 border border-gray-800 overflow-x-auto shadow-inner">
+                        <span class="text-green-500">curl</span> -X GET {{ request.host_url }}api/v1/user/templates \<br>
+                        &nbsp;&nbsp;-H <span class="text-yellow-500">"Authorization: Bearer {{ user_data.api_token or 'YOUR_TOKEN' }}"</span>
+                        
+                        <div class="mt-4 pt-4 border-t border-gray-800/50 text-gray-500">
+                            <span class="text-gray-600">// Expected Response:</span><br>
+                            {<br>
+                            &nbsp;&nbsp;<span class="text-blue-400">"status"</span>: "success",<br>
+                            &nbsp;&nbsp;<span class="text-blue-400">"templates"</span>: ["Basic_Profile", "Advanced_Hosting"]<br>
+                            }
+                        </div>
+                    </div>
+                </div>
+
+                <div class="space-y-2">
+                    <p class="text-[11px] font-bold text-gray-300 uppercase">2. Check if Zone Exists</p>
+                    <div class="bg-[#0b0e14] p-5 rounded-2xl font-mono text-[11px] text-gray-400 border border-gray-800 overflow-x-auto shadow-inner">
+                        <span class="text-green-500">curl</span> -X GET {{ request.host_url }}api/v1/user/zone/check/example.com \<br>
+                        &nbsp;&nbsp;-H <span class="text-yellow-500">"Authorization: Bearer {{ user_data.api_token or 'YOUR_TOKEN' }}"</span>
+                    </div>
+                </div>
+
+                <div class="space-y-2">
+                    <p class="text-[11px] font-bold text-gray-300 uppercase">3. Create New Master Zone</p>
+                    <div class="bg-[#0b0e14] p-5 rounded-2xl font-mono text-[11px] text-gray-400 border border-gray-800 overflow-x-auto shadow-inner">
+                        <span class="text-green-500">curl</span> -X POST {{ request.host_url }}api/v1/user/zone/create \<br>
+                        &nbsp;&nbsp;-H <span class="text-yellow-500">"Authorization: Bearer {{ user_data.api_token or 'YOUR_TOKEN' }}"</span> \<br>
+                        &nbsp;&nbsp;-H "Content-Type: application/json" \<br>
+                        &nbsp;&nbsp;-d '{<br>
+                        &nbsp;&nbsp;&nbsp;&nbsp;<span class="text-blue-400">"domain"</span>: "example.com", <br>
+                        &nbsp;&nbsp;&nbsp;&nbsp;<span class="text-blue-400">"template_name"</span>: "YOUR_TEMPLATE_NAME", <span class="text-gray-600">// REQUIRED</span><br>
+                        &nbsp;&nbsp;&nbsp;&nbsp;<span class="text-blue-400">"sync"</span>: true <br>
+                        &nbsp;&nbsp;}'
+                    </div>
+                </div>
+
+                <div class="space-y-2">
+                    <p class="text-[11px] font-bold text-gray-300 uppercase">4. Add Record (Forward: A, CNAME, TXT etc)</p>
+                    <div class="bg-[#0b0e14] p-5 rounded-2xl font-mono text-[11px] text-gray-400 border border-gray-800 overflow-x-auto shadow-inner">
+                        <span class="text-green-500">curl</span> -X POST {{ request.host_url }}api/v1/user/record/add \<br>
+                        &nbsp;&nbsp;-H <span class="text-yellow-500">"Authorization: Bearer {{ user_data.api_token or 'YOUR_TOKEN' }}"</span> \<br>
+                        &nbsp;&nbsp;-H "Content-Type: application/json" \<br>
+                        &nbsp;&nbsp;-d '{<br>
+                        &nbsp;&nbsp;&nbsp;&nbsp;<span class="text-blue-400">"domain"</span>: "example.com", <br>
+                        &nbsp;&nbsp;&nbsp;&nbsp;<span class="text-blue-400">"name"</span>: "web", <br>
+                        &nbsp;&nbsp;&nbsp;&nbsp;<span class="text-blue-400">"type"</span>: "A", <br>
+                        &nbsp;&nbsp;&nbsp;&nbsp;<span class="text-blue-400">"value"</span>: "1.2.3.4", <br>
+                        &nbsp;&nbsp;&nbsp;&nbsp;<span class="text-blue-400">"sync"</span>: true <br>
+                        &nbsp;&nbsp;}'
+                    </div>
+                </div>
+
+                <div class="space-y-2">
+                    <p class="text-[11px] font-bold text-gray-300 uppercase">5. Add Reverse Record (PTR)</p>
+                    <div class="bg-[#0b0e14] p-5 rounded-2xl font-mono text-[11px] text-gray-400 border border-gray-800 overflow-x-auto shadow-inner">
+                        <span class="text-green-500">curl</span> -X POST {{ request.host_url }}api/v1/user/record/add \<br>
+                        &nbsp;&nbsp;-H <span class="text-yellow-500">"Authorization: Bearer {{ user_data.api_token or 'YOUR_TOKEN' }}"</span> \<br>
+                        &nbsp;&nbsp;-H "Content-Type: application/json" \<br>
+                        &nbsp;&nbsp;-d '{<br>
+                        &nbsp;&nbsp;&nbsp;&nbsp;<span class="text-blue-400">"domain"</span>: "1.168.192.in-addr.arpa", <br>
+                        &nbsp;&nbsp;&nbsp;&nbsp;<span class="text-blue-400">"name"</span>: "10", <br>
+                        &nbsp;&nbsp;&nbsp;&nbsp;<span class="text-blue-400">"type"</span>: "PTR", <br>
+                        &nbsp;&nbsp;&nbsp;&nbsp;<span class="text-blue-400">"value"</span>: "nas.example.com" <br>
+                        &nbsp;&nbsp;}'
+                    </div>
+                </div>
+
+                <div class="space-y-2">
+                    <p class="text-[11px] font-bold text-gray-300 uppercase">6. Manual Cluster Sync</p>
+                    <div class="bg-[#0b0e14] p-5 rounded-2xl font-mono text-[11px] text-gray-400 border border-gray-800 overflow-x-auto shadow-inner">
+                        <span class="text-green-500">curl</span> -X POST {{ request.host_url }}api/v1/user/sync \<br>
+                        &nbsp;&nbsp;-H <span class="text-yellow-500">"Authorization: Bearer {{ user_data.api_token or 'YOUR_TOKEN' }}"</span> \<br>
+                        &nbsp;&nbsp;-H "Content-Type: application/json" \<br>
+                        &nbsp;&nbsp;-d '{<span class="text-blue-400">"domain"</span>: "example.com"}'
+                    </div>
+                </div>
+
+            </div>
+        </div>
+    </div>
+</div>
+{% endblock %}