| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607 |
- {% extends "layout.html" %}
- {% block content %}
- <div id="zone-dynamic-content">
- {% set is_reverse = '.arpa' in domain %}
- {% set friendly_name = domain %}
- {% set base_ip = '' %}
- {% if is_reverse %}
- {% set rev_parts = domain.replace('.in-addr.arpa', '').replace('.ip6.arpa', '').split('.') %}
- {% set base_ip = rev_parts | reverse | join('.') %}
- {% set friendly_name = base_ip ~ '.x (Reverse)' %}
- {% endif %}
- <div class="flex justify-between items-center mb-8 bg-[#12161f] p-6 rounded-2xl border border-gray-800 shadow-2xl">
- <div>
- <h2 class="text-3xl font-black font-mono text-blue-400 tracking-tighter">{% if domain == '.' %}. (Root Zone){% else %}{{ friendly_name }}{% endif %}</h2>
- {% if is_reverse %}<p class="text-[10px] text-gray-500 font-mono tracking-widest mt-1">{{ domain }}</p>{% endif %}
-
- <div class="mt-2 flex items-center gap-3">
- <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 %}
- {% 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|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">
- <button type="button" onclick="openConvertToSlaveModal()" class="text-[9px] bg-purple-600/20 text-purple-400 px-2 py-1 rounded-lg font-black uppercase transition border border-purple-500/30">Convert to Slave</button>
- </div>
- {% elif type == 'hint' %}
- <span class="text-[10px] bg-red-900/30 text-red-500 px-3 py-1 rounded-full uppercase font-black border border-red-800/50">ROOT CONFIG</span>
- {% elif type == 'forward' %}
- <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">FORWARDER</span>
- {% endif %}
- </div>
- </div>
- <a href="/" class="text-gray-500 hover:text-white font-bold text-sm transition flex items-center gap-2">← Back</a>
- </div>
- <div class="flex border-b border-gray-800 mb-6 gap-6 overflow-x-auto whitespace-nowrap scrollbar-hide">
- <button onclick="switchMainTab('records')" id="mainTab_records" class="pb-4 text-[10px] font-black uppercase tracking-widest text-blue-500 border-b-2 border-blue-500 transition focus:outline-none">DNS Records</button>
- {% if type == 'master' %}
- <button onclick="switchMainTab('audit')" id="mainTab_audit" class="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">Audit & Time Machine</button>
- <button onclick="switchMainTab('security')" id="mainTab_security" class="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">Security (DNSSEC)</button>
- {% endif %}
- </div>
- <div id="mainSection_records" class="main-section">
- {% 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|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>
- </div>
- <div class="w-full lg:w-32">
- <select name="rtype" id="addRtype" onchange="updateValidation('add')" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl px-3 h-[46px] text-white font-bold outline-none cursor-pointer text-sm">
- {% if is_reverse %}
- <option value="PTR">PTR</option>
- <option value="NS">NS</option>
- {% else %}
- <option value="A">A</option><option value="AAAA">AAAA</option><option value="CNAME">CNAME</option><option value="MX">MX</option><option value="TXT">TXT</option><option value="NS">NS</option><option value="SRV">SRV</option>
- {% endif %}
- </select>
- </div>
- <div class="flex-1 w-full">
- <input type="text" name="value" id="addValue" required placeholder="Value" 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 id="addHelper" class="text-[9px] text-gray-500 font-black tracking-widest uppercase mt-2 h-3 transition-colors"></p>
- </div>
- <button type="submit" class="w-full lg:w-auto bg-blue-600 hover:bg-blue-500 text-white px-10 h-[46px] rounded-xl font-black uppercase text-[11px] tracking-widest transition shadow-xl">Add</button>
- </form>
- </div>
- {% elif type == 'slave' %}
- <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|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>
- </div>
- <div class="bg-black/50 p-4 rounded-xl font-mono text-[10px] h-40 overflow-y-auto custom-scroll flex flex-col-reverse shadow-inner">
- {% if logs %}
- {% for log in logs %}
- <div class="mb-1 text-gray-500 border-b border-gray-900 last:border-0 pb-1">{{ log }}</div>
- {% endfor %}
- {% else %}
- <div class="text-gray-700 italic font-bold">No recent logs found or transfer pending.</div>
- {% endif %}
- </div>
- </div>
- {% endif %}
- {% if type != 'forward' %}
- {% if has_records %}
- <div class="flex border-b border-gray-800 mb-6 gap-2 overflow-x-auto custom-scroll pb-1">
- {% for rtype, rlist in records.items() if rlist %}
- <button onclick="switchTab('{{ rtype }}')" id="btn-{{ rtype }}" class="tab-btn px-6 py-3 text-[11px] font-black uppercase tracking-widest border-b-2 transition-all flex items-center gap-2 {% if loop.first %}border-blue-500 text-blue-400{% else %}border-transparent text-gray-500 hover:text-gray-300{% endif %}">
- {{ rtype }} <span class="bg-gray-800/80 text-gray-400 px-2 py-0.5 rounded-full text-[9px]">{{ rlist|length }}</span>
- </button>
- {% endfor %}
- </div>
- <div class="bg-[#12161f] border border-gray-800 rounded-2xl overflow-hidden shadow-2xl min-h-[200px]">
- {% for rtype, rlist in records.items() if rlist %}
- <div id="tab-{{ rtype }}" class="tab-content {% if not loop.first %}hidden{% endif %}">
- <table class="w-full text-sm text-left font-mono">
- <thead class="bg-black/20 text-[10px] font-black text-gray-500 uppercase tracking-widest border-b border-gray-800">
- <tr><th class="px-6 py-4 w-1/3">Name / IP</th><th class="px-6 py-4">Value</th><th class="px-6 py-4 text-right">Actions</th></tr>
- </thead>
- <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' %}
- 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 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 %}
- </td>
- </tr>
- {% endfor %}
- </tbody>
- </table>
- </div>
- {% endfor %}
- </div>
- {% if type == 'master' %}
- <p class="text-center text-[10px] text-gray-500 font-bold mt-4 uppercase tracking-widest animate-pulse">Tip: Double-click any row to edit instantly</p>
- {% 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>
- <p class="text-gray-500 font-bold uppercase tracking-widest text-[11px]">No records found yet.</p>
- </div>
- {% endif %}
- {% endif %}
- </div>
- <div id="mainSection_audit" class="main-section hidden">
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-8">
- <div class="bg-[#12161f] border border-gray-800 rounded-2xl shadow-xl flex flex-col h-[500px]">
- <div class="p-6 border-b border-gray-800 bg-gray-900/30"><h3 class="text-lg font-black text-white uppercase tracking-tighter italic">Audit Log (Recent Changes)</h3></div>
- <div class="flex-1 overflow-y-auto custom-scroll p-6 font-mono text-[11px]">
- {% if audit_logs %}
- {% for log in audit_logs %}
- <div class="mb-2 pb-2 border-b border-gray-800 last:border-0 text-gray-400">
- <span class="text-gray-600 block mb-1">{{ log.time }}</span>
- <span class="text-blue-400 font-bold">{{ log.user }}</span>: {{ log.action }}
- </div>
- {% endfor %}
- {% else %}
- <p class="text-gray-600 text-center italic mt-10">No recent changes found.</p>
- {% endif %}
- </div>
- </div>
-
- <div class="bg-[#12161f] border border-gray-800 rounded-2xl shadow-xl flex flex-col h-[500px]">
- <div class="p-6 border-b border-gray-800 bg-gray-900/30"><h3 class="text-lg font-black text-yellow-500 uppercase tracking-tighter italic flex items-center gap-2"><svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>Time Machine</h3></div>
- <div class="flex-1 overflow-y-auto custom-scroll p-6 font-mono text-[11px]">
- {% if backups %}
- {% for b in backups %}
- <div class="mb-3 pb-3 border-b border-gray-800 last:border-0 flex justify-between items-center group">
- <div>
- <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|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>
- {% endfor %}
- {% else %}
- <p class="text-gray-600 text-center italic mt-10 text-sm">No backups found. Make a change to create one!</p>
- {% endif %}
- </div>
- </div>
- </div>
- </div>
- <div id="mainSection_security" class="main-section hidden">
- <div class="bg-[#12161f] border border-gray-800 rounded-2xl shadow-xl p-8 max-w-4xl mx-auto text-center">
- <h2 class="text-3xl font-black uppercase tracking-tighter italic mb-4 text-white">DNSSEC Activation</h2>
- <p class="text-gray-400 text-sm font-bold mb-8">Secure your domain against spoofing with a single click. BIND will handle cryptographic signatures automatically (inline-signing).</p>
-
- <div class="flex flex-col items-center justify-center p-8 rounded-xl border-2 {% if dnssec_enabled %}border-green-500/30 bg-green-900/10{% else %}border-gray-800 bg-black/40{% endif %} mb-8">
- {% if dnssec_enabled %}
- <svg class="w-16 h-16 text-green-500 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path></svg>
- <h3 class="text-2xl font-black text-green-400 uppercase tracking-widest mb-2">Active & Protected</h3>
- <p class="text-gray-400 text-xs mb-6">Zone is cryptographically signed via BIND inline-signing.</p>
- {% else %}
- <svg class="w-16 h-16 text-gray-600 mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg>
- <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|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 %}
- <button type="submit" class="bg-green-600 text-white hover:bg-green-500 px-8 py-3 rounded-xl font-black uppercase transition text-xs shadow-xl flex items-center gap-2"><svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"></path></svg> Enable DNSSEC</button>
- {% endif %}
- </form>
- </div>
- {% if dnssec_enabled and ds_records %}
- <div class="text-left bg-black/60 p-6 rounded-xl border border-gray-800">
- <h4 class="text-sm font-black text-blue-400 uppercase tracking-widest mb-4">DS Records for Registrar</h4>
- <p class="text-xs text-gray-400 mb-4">Copy and paste these Delegation Signer (DS) records to your Domain Registrar (e.g. GoDaddy, Namecheap) to complete the chain of trust.</p>
- <div class="space-y-2">
- {% for ds in ds_records %}
- <input type="text" readonly value="{{ ds }}" class="w-full bg-[#0b0e14] border border-gray-800 rounded-lg p-3 text-green-400 font-mono text-[10px] outline-none cursor-pointer hover:border-green-500/50 transition" onclick="this.select(); document.execCommand('copy'); AppToast.fire({icon:'success', title:'Copied to clipboard!'});">
- {% endfor %}
- </div>
- </div>
- {% elif dnssec_enabled %}
- <p class="text-xs text-yellow-500 font-bold italic">Keys are generating in the background. It may take a minute for DS records to appear here.</p>
- {% endif %}
- </div>
- </div>
- <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|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">
- <label class="block text-[10px] font-black text-gray-500 uppercase mb-2">Name / IP</label>
- <input type="text" name="name" id="editName" required class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-4 text-white outline-none focus:border-blue-500 transition-colors">
- </div>
- <div>
- <label class="block text-[10px] font-black text-gray-500 uppercase mb-2">Type</label>
- <input type="text" name="rtype" id="editRtype" readonly class="w-full bg-gray-900/50 border border-gray-800 rounded-xl p-4 text-gray-500 font-black outline-none cursor-not-allowed">
- </div>
- </div>
- <div>
- <label class="block text-[10px] font-black text-gray-500 uppercase mb-2">Value</label>
- <input type="text" name="value" id="editValue" required class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-4 text-white outline-none focus:border-blue-500 transition-colors">
- <p id="editHelper" class="text-[9px] text-gray-500 font-black tracking-widest uppercase mt-2 h-3 transition-colors"></p>
- </div>
- <div class="flex gap-4 pt-6">
- <button type="button" onclick="closeEditModal()" class="flex-1 bg-gray-800 hover:bg-gray-700 transition-colors text-gray-400 font-bold py-4 rounded-xl uppercase text-xs">Cancel</button>
- <button type="submit" class="flex-1 bg-blue-600 hover:bg-blue-500 transition-colors text-white font-black py-4 rounded-xl uppercase text-xs flex justify-center items-center">Save Changes</button>
- </div>
- </form>
- </div>
- </div>
- <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|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">
- <button type="button" onclick="closeEditIpsModal()" class="flex-1 bg-gray-800 text-gray-400 transition-colors font-bold py-3 rounded-xl uppercase text-xs">Cancel</button>
- <button type="submit" class="flex-1 bg-blue-600 text-white font-black py-3 rounded-xl uppercase text-xs shadow-xl">Save</button>
- </div>
- </form>
- </div>
- </div>
- <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|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">
- <input type="text" name="master_ips[]" placeholder="Master IP" required class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-3 text-white font-mono text-sm outline-none focus:border-purple-500 transition-colors">
- </div>
- </div>
- <button type="button" onclick="addSlaveIpField()" class="w-full py-3 border-2 border-dashed border-gray-800 rounded-xl text-gray-500 text-[11px] font-black uppercase transition hover:text-gray-300">+ Add Another IP</button>
- <div class="flex gap-4 pt-6">
- <button type="button" onclick="closeConvertToSlaveModal()" class="flex-1 bg-gray-800 text-gray-400 font-bold py-3 rounded-xl uppercase text-xs">Cancel</button>
- <button type="submit" class="flex-1 bg-purple-600 hover:bg-purple-500 text-white font-black py-3 rounded-xl uppercase text-xs shadow-xl transition">Convert</button>
- </div>
- </form>
- </div>
- </div>
- <div id="digModal" class="hidden fixed inset-0 bg-black/95 backdrop-blur-sm items-center justify-center z-[100] p-6">
- <div class="bg-[#12161f] border border-gray-800 w-full max-w-2xl rounded-3xl p-8 shadow-2xl flex flex-col max-h-[90vh]">
- <div class="flex justify-between items-center mb-6 border-b border-gray-800 pb-4">
- <h3 class="text-xl font-black text-green-400 uppercase italic">AXFR Transfer Simulation</h3>
- <button onclick="closeDigModal()" class="text-gray-500 hover:text-white text-2xl font-bold">×</button>
- </div>
-
- <div id="digLoading" class="py-20 text-center">
- <div class="animate-spin rounded-full h-12 w-12 border-b-2 border-green-500 mx-auto mb-4"></div>
- <p class="text-gray-500 font-black uppercase text-xs tracking-widest">Running Dig AXFR...</p>
- </div>
- <div id="digContent" class="hidden overflow-y-auto">
- <div class="grid grid-cols-2 gap-4 mb-6" id="digAnalysis">
- </div>
-
- <h4 class="text-[10px] font-black text-gray-500 uppercase mb-2 tracking-widest">Raw Output</h4>
- <pre id="digRaw" class="bg-black p-4 rounded-xl text-[10px] text-green-600 font-mono overflow-x-auto border border-gray-800 custom-scroll"></pre>
- </div>
- <div class="mt-8">
- <button onclick="closeDigModal()" class="w-full bg-gray-800 hover:bg-gray-700 py-4 rounded-xl text-white font-black uppercase text-xs transition">Close</button>
- </div>
- </div>
- </div>
- </div>
- <script>
- let activeRecordTab = '';
- let activeMainTab = 'records';
- const recordRules = {
- 'A': { regex: /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/, msg: "Must be a valid IPv4 address", placeholder: "192.168.1.1" },
- 'AAAA': { regex: /^[a-fA-F0-9:]+$/, msg: "Must be a valid IPv6 address", placeholder: "2001:db8::1" },
- 'CNAME': { regex: /^[a-zA-Z0-9\-\_\.]+$/, msg: "Must be a domain without spaces", placeholder: "target.example.com." },
- 'NS': { regex: /^[a-zA-Z0-9\-\_\.]+$/, msg: "Must be a valid Nameserver domain", placeholder: "ns1.example.com." },
- 'TXT': { regex: /^.*$/, msg: "Any text value (Usually wrapped in quotes)", placeholder: '"text here"' },
- 'MX': { regex: /^\d+\s+[a-zA-Z0-9\-\_\.]+$/, msg: "Format: Priority Domain", placeholder: "10 mail.example.com." },
- 'SRV': { regex: /^\d+\s+\d+\s+\d+\s+[a-zA-Z0-9\-\_\.]+$/, msg: "Format: Priority Weight Port Domain", placeholder: "10 5 5060 sip.com." },
- 'PTR': { regex: /^[a-zA-Z0-9\-\_\.]+$/, msg: "Must be a valid domain name", placeholder: "host.example.com." }
- };
- // --- SPA & AJAX SMART PARSER ---
- 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; }
-
- 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!' });
- }
- } 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!' });
- }
- } 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));
- });
- });
- });
- }
- function updateValidation(prefix) {
- const typeEl = document.getElementById(prefix + 'Rtype');
- const inputEl = document.getElementById(prefix + 'Value');
- const helperEl = document.getElementById(prefix + 'Helper');
- if(!typeEl || !inputEl || !helperEl) return;
- const rule = recordRules[typeEl.value];
- inputEl.placeholder = rule.placeholder;
- helperEl.innerText = rule.msg;
- inputEl.setCustomValidity('');
- inputEl.oninput = function() {
- let isValid = rule.regex.test(inputEl.value.trim());
- inputEl.setCustomValidity(isValid ? '' : rule.msg);
- helperEl.className = "text-[9px] font-black tracking-widest uppercase mt-2 h-3 " + (isValid ? "text-gray-500" : "text-red-500");
- };
- if(inputEl.value) inputEl.oninput();
- }
- function switchMainTab(tab) {
- activeMainTab = tab;
- document.querySelectorAll('.main-section').forEach(el => el.classList.add('hidden'));
- document.querySelectorAll('[id^="mainTab_"]').forEach(btn => {
- btn.classList.remove('border-blue-500', 'text-blue-500');
- btn.classList.add('border-transparent', 'text-gray-500');
- });
-
- 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');
- btn.classList.replace('text-gray-500', 'text-blue-500');
- }
- }
- function switchTab(selectedType) {
- activeRecordTab = selectedType;
- document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden'));
- document.querySelectorAll('.tab-btn').forEach(btn => {
- btn.classList.remove('border-blue-500', 'text-blue-400');
- btn.classList.add('border-transparent', 'text-gray-500');
- });
- const tabEl = document.getElementById('tab-' + selectedType);
- if(tabEl) tabEl.classList.remove('hidden');
- const activeBtn = document.getElementById('btn-' + selectedType);
- if(activeBtn) {
- activeBtn.classList.replace('border-transparent', 'border-blue-500');
- activeBtn.classList.replace('text-gray-500', 'text-blue-400');
- }
- }
- function restoreRecordTab() {
- switchMainTab(activeMainTab);
- if(document.getElementById('addRtype')) updateValidation('add');
- if(activeRecordTab && document.getElementById('btn-' + activeRecordTab)) {
- switchTab(activeRecordTab);
- } else {
- const firstBtn = document.querySelector('.tab-btn');
- if(firstBtn) switchTab(firstBtn.id.replace('btn-', ''));
- }
- initTableSort(document.getElementById('zone-dynamic-content'));
- }
- // ΔΙΟΡΘΩΣΗ: Ασφαλής ανάγνωση των 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(() => {
- const input = document.getElementById('editValue');
- input.focus();
- input.setSelectionRange(input.value.length, input.value.length);
- }, 50);
- }
- function closeEditModal() { document.getElementById('editModal').classList.replace('flex', 'hidden'); }
- function openEditIpsModal(currentIps) {
- const cont = document.getElementById('editIpsContainer');
- cont.innerHTML = '';
- currentIps.split(' ').forEach(ip => { if(ip.trim()) addEditIpField(ip.trim()); });
- document.getElementById('editIpsModal').classList.replace('hidden', 'flex');
- }
- function closeEditIpsModal() { document.getElementById('editIpsModal').classList.replace('flex', 'hidden'); }
- function addEditIpField(val) {
- const div = document.createElement('div'); div.className = 'flex gap-2';
- div.innerHTML = `<input type="text" name="ips[]" value="${val}" class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-3 text-white font-mono text-sm outline-none focus:border-blue-500 transition-colors"><button type="button" onclick="this.parentElement.remove()" class="text-gray-600 hover:text-red-500 px-3 font-black text-xl transition-colors">×</button>`;
- document.getElementById('editIpsContainer').appendChild(div);
- }
- function openConvertToSlaveModal() { document.getElementById('convertToSlaveModal').classList.replace('hidden', 'flex'); }
- function closeConvertToSlaveModal() { document.getElementById('convertToSlaveModal').classList.replace('flex', 'hidden'); }
- function addSlaveIpField() {
- const div = document.createElement('div'); div.className = 'flex gap-2 mt-2';
- div.innerHTML = `<input type="text" name="master_ips[]" placeholder="Master IP" required class="w-full bg-[#0b0e14] border border-gray-800 rounded-xl p-3 text-white font-mono text-sm outline-none focus:border-purple-500 transition-colors"><button type="button" onclick="this.parentElement.remove()" class="text-gray-600 hover:text-red-500 px-3 font-black text-xl transition-colors">×</button>`;
- document.getElementById('slaveIpsContainer').appendChild(div);
- }
- function runDigTest(domain) {
- const modal = document.getElementById('digModal');
- const loading = document.getElementById('digLoading');
- const content = document.getElementById('digContent');
- const analysisCont = document.getElementById('digAnalysis');
- const rawCont = document.getElementById('digRaw');
- modal.classList.replace('hidden', 'flex');
- loading.classList.remove('hidden');
- content.classList.add('hidden');
- fetch('/api/zones/test_dig', {
- method: 'POST',
- headers: {'Content-Type': 'application/json'},
- body: JSON.stringify({domain: domain, target_ip: '127.0.0.1'})
- })
- .then(r => r.json())
- .then(data => {
- loading.classList.add('hidden');
- content.classList.remove('hidden');
-
- if(data.success) {
- analysisCont.innerHTML = `
- <div class="bg-green-900/10 border border-green-500/20 p-4 rounded-2xl">
- <p class="text-[9px] text-gray-500 uppercase font-black">Total Records</p>
- <p class="text-2xl font-black text-white">${data.total}</p>
- </div>
- <div class="bg-blue-900/10 border border-blue-500/20 p-4 rounded-2xl flex flex-wrap gap-2 items-center">
- ${Object.entries(data.analysis).map(([type, count]) => `
- <div class="flex items-center gap-1 bg-black/40 px-2 py-1 rounded border border-gray-800">
- <span class="text-blue-400 font-bold text-[10px]">${type}:</span>
- <span class="text-white font-black text-[10px]">${count}</span>
- </div>
- `).join('')}
- </div>
- `;
- } else {
- analysisCont.innerHTML = `<div class="col-span-2 bg-red-900/20 border border-red-500/30 p-4 rounded-xl text-red-500 font-bold text-xs">Error: ${data.message}</div>`;
- }
- rawCont.innerText = data.raw;
- })
- .catch(err => {
- loading.classList.add('hidden');
- content.classList.remove('hidden');
- analysisCont.innerHTML = `<div class="col-span-2 bg-red-900/20 border border-red-500/30 p-4 rounded-xl text-red-500 font-bold text-xs">API Connection Failed</div>`;
- });
- }
- function closeDigModal() { document.getElementById('digModal').classList.replace('flex', 'hidden'); }
- document.addEventListener("DOMContentLoaded", () => { restoreRecordTab(); });
- </script>
- {% endblock %}
|